Getting started

dynrules uses a simple, but flexible class layout to configure rulesets and create scripts from them. In order to use the dynrules package, simply import it using

import dynrules

You now can start to create own rulesets and set up your script generator.

Creating rules - Rule

A rule defines certain criteria of an object’s behaviour. The Rule class features the most basic needs to set up your own rules for scripts. It consists of an id, a weight and code that defines the rule’s action.

Let’s imagine, you have a game with a Warrior class that can walk in four directions and fight against enemies. Defining those actions might look like

class Warrior:
    def do_walk (self, direction):
        ...
    def fight (self, enemy):
        ...
    ...

Automating walking and fighting requires a lot of work regarding the priorities of when to walk and when to fight. Fighting a stronger enemy might be senseless, so the Warrior should walk away from it. Let’s try to formulate some rules for it:

if is_enemy_at (direction):
    if warrior.strength > enemy.strength:
        warrior.fight (enemy)
    else:
        warrior.do_walk (~direction)
else:
    warrior.do_walk (direction)

What does the above code do? First it checks, whether there is an enemy at the specified direction the warrior should walk to. If it is, and it is weaker than the warrior, the warrior will fight it. Otherwise, the warrior will go into the opposite direction to escape a possible fight. At last, if no enemy is found at the given direction, the warrior will walk towards it.

This is a predictable, typical behaviour and sometimes it might happen that, although the warrior is weaker, it will fight the enemy with success. That means, for a more unpredictable behaviour, we have four possible actions:

  1. warrior.strength > enemy.strength: fight enemy
  2. warrior.strength < enemy.strength: flee from enemy
  3. warrior.strength < enemy.strength: fight enemy
  4. warrior.strength > enemy.strength: flee from enemy

The first and second rule are directly from the solution above. They make the most sense in those cases and should be preferred. Rule three and four shall offer some more unpredictable behaviour.

Let’s formulate some rules for the enemy detection scenario using dynrules.

# Create a new Rule for fighting the enemy.
rule1 = Rule (1)
rule1.weight = 10
rule1.code = "if warrior.strength > enemy.strength: warrior.fight (enemy)"

# Create another Rule for fleeing.
rule2 = Rule (2)
rule2.weight = 10
rule2.code = "if warrior.strength < enemy.strength: warrior.do_walk (~direction)"

# Fighting a stronger enemy
rule3 = Rule (3)
rule3.weight = 5
rule3.code = "if warrior.strength < enemy.strength: warrior.fight (enemy)"

# Fleeing from a weaker enemy
rule4 = Rule (4)
rule4.weight = 5
rule4.code = "if warrior.strength > enemy.strength: warrior.do_walk (~direction)"

We set up the necessary rules. Now it’s time to put them together in a RuleSet that takes care of them.

Weighting rules - RuleSet

Weighting a rule means to mark and measure its priority or importance within a set of applicable rules. The more important a rule is, the higher its weight should be. Measuring the importance of a rule is usually done by counting how often it is called. The result (successful or unsuccessful) often influences the rule’s weight, but does not need to.

The RuleSet takes care of mesuring the importance and updating the weight of the Rule objects it contains. A RuleSet usually consists of applicable rules for a specific situation and lets you define methods for measuring and detecting the success of the rules. As the process of measuring the success solely depends on the surrounding system, the implemenation of that process can vary and the RuleSet class requires you to take care of it.

class MyOwnRuleSet (RuleSet):
    def calculate_adjustment (self, fitness):
        # Implement your own success detection here.
        pass

    def distribute_remainder (self, remainder):
        # Implement your own remainder method here.
        pass

calculate_adjustment(self, fitness) calculates the reward or penalty, each used rule receives. The fitness argument can be used to provide additional information, e.g. about the performance of the execution.

distribute_remainder (self, remainder) is called to distribute the difference between the total weight before and after the update once the weight updating within the RuleSet is done. This might be necessary to allow a balancing of rule weights so that the total sum of all rules within a RuleSet will remain the same, for example. In reality however, such a distribution solely depends on the specific application needs.

The weight update process of the RuleSet looks like

def update_weights (self, fitness):
    # Initialise needed things.
    adjustment = self.calculate_adjustment (fitness)
    # Update rule weights with the adjustment and calculuate remainder
    self.distribute_remainder (remander)
    # Update new total weight.
    # return

To get a better idea about this, let’s create a small RuleSet implementation for our previously created rules. We assume that the fitness we receive expresses the difference between the damage the warrior made and received during the execution of the rules.

class WarriorRuleSet (RuleSet):
    def calculate_adjustment (self, fitness):
        #
        # fitness = damage_warrior_caused - damage_warrior_received
        #
        # 1) a high fitness means, the warrior caused more damage
        # 2) a very low or negative fitness means, the warrior did not
        # cause that much damage or even received more than it caused.
        #
        # for case 1) we assume the execution of the rules to be
        # successful, for case 2) we do not.

        # We set the sucess/fail threshold to 3.
        if fitness > 3:
            # The execution was successful, the warrior is strong!
            # The adjustment will be the total fitness - threshold.
            return fitness - 3
        else:
            # The execution was not successful, the warrior is weak!
            if fitness < 0:
                # Lousy, simply return the negative fitness
                return fitness
            else:
                # Not so lousy, return a penalty value as difference
                # of threshold minus fitness.
                return - (3 - fitness)

    def distribute_remainder (self, remainder):
        #
        # Here we distribute the difference of the last total weights
        # and newly calculated total weights.
        # Give each rule the same fraction.
        #
        count = len (self.rules)
        if count == 0:
            return # Safety net, if no rules are there.

        fraction = remainder / float (count)
        for rule in self.rules:
            rule.weight += fraction

Now we can add the created rules from above.

warriorruleset = WarriorRuleSet (0, 20)
warriorruleset.add (rule1)
warriorruleset.add (rule2)
warriorruleset.add (rule3)
warriorruleset.add (rule4)

The both arguments of the constructor, minweight and maxweight are the boundary limits for rules contained in a RuleSet. They define the upper and lower weight limit, each rule can have.

From now on, the WarriorRuleSet is fully functional and can update rule weights as necessary.

To add another level of automation and to create scripts from the rules, a LearnSystem will be necessary however.

Generating scripts - LearnSystem

The LearnSystem class is used to create scripts automatically from an existing RuleSet. It generates the scripts in a programming language neutral manner which means, that it only uses the code attribute of Rule objects for creating the output.

Additionally the LearnSystem can add code to be executed before and after the rules are entered to make the generated script fully functional for the specific task and environment. The creation of a script thus consists of the following tasks.

  1. Create script header
  2. Select rules and create code
  3. Create script footer

To create a LearnSystem for the WarriorRuleSet, only a single line of code is necessary.

warriorlearnsystem = LearnSystem (warriorruleset)

The LearnSystem is now full functional and you can start generating scripts for the warrior.

warriorlearnsystem.create_script ("scriptfile.scr", 4)

create_script will create a new script, insert a header, add rules to it and then add the footer. You can specify the maximum amount of rules to be added by setting the second argument to the required value.

# Add a maximum of 10 rules.
warriorlearnsystem.create_script ("scriptfile.scr", 10)
# Add a maximum of 3 rules.
warriorlearnsystem.create_script ("scriptfile2.scr", 3)

You can modify several attributes and methods of the LearnSystem to tweak it to your personal needs.

create_header() and create_footer() are used to create necesary code to add before and after the rules. That can be initialisation and finalisation code, checks or whatever is necessary for the target system. Both methods return a string containing the code to add.

class OwnLearnSystem (LearnSystem):
    def create_header (self):
        # Create header code
        return 'def execute_rules (object):\n' + \
               '    selected_rule = None\n'

    def create_footer (self):
        # Create footer code
        return '    return selected_rule\n'

The above class would generate the following code:

def execute_rules (object):
    selected_rule = None
    #
    # RULE CODE
    #
    return selected_rule

The maxscriptsize attribute allows you to define the maximum size in bytes of a script to generate. maxscriptsize does not take the header and footer into account, but only the code generated from the rules.

# Limit the size of the code generated from the rules to 4 kB.
warriorlearnsystem.maxscriptsize = 4096

maxtries limits the rule selection process, so that it does not take infinite trials to find a rule to add. This can be very helpful to limit the time spent on selecting rules.

# Only try to find new rules 50 times.
warriorlearnsystem.maxtries = 50

That’s it. We now have a basic dynamic scripting system that can select rules, create scripts and update the rule weights upon execution of the scripts. Now it is time to integrate all of it into the AI logic code!

Project Versions

Table Of Contents

Previous topic

Introduction

Next topic

Python API reference

This Page