Alex
Arcane
Hi people. Lately, I have been wracking my head over an idea I had for a more friendly computer language paradigm for implementing game rules. I have come up with a few concepts and decided to share them over here, as other people may have other ideas, or know of similar ones.
So, the way I see things, it isn't very comfortable to implement tabletop rpg or strategy game rules, or rules like those, in an object oriented language. These kind of rules are written with a different mindset. When you write in object oriented languages (at least traditional ones), you have various little "objects" that have a "type". These types determine its object behavior, what it is capable of. The type defines what its objects do and how they do it. This, however, means that the types must somehow feature in the rules you describe in your system. If an object is supposed to be able to attack another, the type must have a "method" that allows it to do so. In other words, there should be something like "attack(target)" defined in the class, which will have rules for dealing with an attack from an object of that class to a provided target.
My problem with this model is that this isn't how people think when they are coming up with rules. For example, in a wargame like Warhammer, there could be simple rules describing how units go about attacking. So far so good, the attack routine depends mostly on attributes of the unit itself, each unit having things like strength, number of attacks, hit points, etc. But then, it will probably start complicating things with exceptions. It might have special rules for when ranged units enter hand to hand, like their attack strength is reduced by two. Then it might have special rules for orcs, like orcs doubling their strength when charging. Then it might have special rules for orc ranged units, that different from normal ranged units, are able to charge, but they don't get the doubled strength effect. Then it might have special rules for spellcaster, which might happen to be an orc ranged unit as well.
My point here is that rules, as we think of them, don't depend so much on fixed types, but usually depends on predicates. Predicates are simply something that can be said about game elements. For example, I might say an element x is an orc, or a ranged unit or a spellcaster, or all of these, or even none. Each is verifiable in some way, though this verification may rely on axioms. For example, the orc predicate might be based on an axiom (elements either are or are not orcs), while the ranged unit predicate might be based on checking to see if the unit has any attack with range greater than one. Based on these predicates, we define the basic behavior and exceptions of this basic behavior. So, my idea is to make a computer language that follows this paradigm, so that programming in it is more natural and debugging it is easier.
The basic idea is, then, that we have these various "typeless" objects. These objects already have the inherent capacity to store fields in a key->value mapping way. So, a given object might store in the key "name" the value "Albert", and then, if asked the value of the field "name", it would answer "Albert". Also, let's assume we have a few basic objects from out of the system that provide us with functionalities such as numbers, arrays and strings (this isn't really necessary, but will make the idea easier to follow).
Besides these objects, we also have "predicates". Predicates are special in that they are able to check if an object is part of it. For example, the predicate "orc(x)" might tell if an object x is an orc, possibly by checking if the field "race" of x is set to "orc". Predicates aren't limited to one object, either. A predicate enemy(x,y) might tell if x is an enemy of y (which might be important in a wargame when determining issues such as friendly fire, etc). Predicates are also semi-ordered, meaning that some more specific predicates have greater precedence than less specific ones. For example, we might have "orc" as a predicate and "black orc" as a more specific predicate, so black orc is "greater" than orc, but another predicate, like "spellcaster" might not be comparable to either "orc" or "black orc". By the way, if we have two predicates, p and q, and q is greater than p, then if q(x) is true, then p(x) is also true.
Also, we have methods. Methods are associated with four things. First, methods have a name, such as attack, or defend, or getStrength. Second, methods are associated to a cardinality, which is the number of arguments it receives. For example, a method like "defend" might receive only one argument, so that defend(x) means that the object x will try to defend itself, while "attack" might receive two, attack(x,y) meaning that x is attacking y.
Third, each method is associated with a "body", a description of some kind of how to run the method. A method called "getStrength(x)" might simply return the value of the "strength" property of x, while a method getStrengthBonus(x), might take the value given by getStrength(x), divide it by two, subtract five and return the total. Finally (and most interesting), each method is associated with a predicate of equal cardinality. This association means that the method is available to all objects where the predicate hods true. For example, a method like "getSpellPoints(x)" might be associated with a "spellcaster(x)" predicate, so only spellcasters have that method available, whereas "attack(x,y)" might be associated with "enemy(x,y)", so it is only legal for x to attack y if he is an enemy of y. There is also polymorphism in the case that a predicate is greater than another. So, in the orc(x) and black_orc(x) predicate examples, if both have methods called "blood_lust(x)", then the method associated with black_orc will be used instead of the more general one if x is a black_orc.
Finally, the last element we add are special cases. Special cases are all associated with one (or, in a simple extension, one or more) methods. Each special case is comprised of one predicate (possibly a composite one) over the method's variables and a "body". The idea behind special cases is that, whenever you are executing a method it is associated to, if its predicate is valid, then its body is executed. The predicate's body may alter or substitute the original body of the method. So, you might have a method, say, damage(x,y), where x is a character and y is a damage (a special object that encapsulates damage amount and type). We might have a special rule for this method with the predicate (undead(x) & fire(y)) (meaning x is an undead and y is fire damage) and the body "damage(x, 2*y). Note that rules are only applied once, so this doesn't end up in an endless loop.
Each method orders its special cases, so they are executed in the desired sequence (useful when, for example, a few cases add to values whereas others multiply them). Also, one special case may override another, meaning that if its predicate is true, the overridden one isn't checked or executed. Any and all special cases that apply for a particular method are executed in the defined order. So, for example, in the getStrength(x) method, the special case {ghost(x) :- return 0} might override all other considerations, so if x is a ghost, its strength will always be 0.
Finally, we may determine that predicates are able to check the execution stack. This is useful for creating rules like "the strength of orc units double when charging". Simply implement a special case for getStrength(x) whose predicate checks to see if charge(x,y) is further up in the stack. The predicate might be something like charging_orcs(x, y), and it checks if there is a method call charge(x,y) with x being orcs in the stack.
Well, this is it. I am sure I m forgetting something, but I need to sleep now and there is no point in posting every detail in one go. I hope you people forgive my poor communication skills. If anything in the post is unintelligible, ask about it and I will be happy to try to clarify. My objective with this is creating a language where it is easy to create and modify rule sets for games on the fly, where it is easy to test different rule designs before one sets for something definitive or even to allow the rule set to evolve as one develops new games in the same engine.
So, the way I see things, it isn't very comfortable to implement tabletop rpg or strategy game rules, or rules like those, in an object oriented language. These kind of rules are written with a different mindset. When you write in object oriented languages (at least traditional ones), you have various little "objects" that have a "type". These types determine its object behavior, what it is capable of. The type defines what its objects do and how they do it. This, however, means that the types must somehow feature in the rules you describe in your system. If an object is supposed to be able to attack another, the type must have a "method" that allows it to do so. In other words, there should be something like "attack(target)" defined in the class, which will have rules for dealing with an attack from an object of that class to a provided target.
My problem with this model is that this isn't how people think when they are coming up with rules. For example, in a wargame like Warhammer, there could be simple rules describing how units go about attacking. So far so good, the attack routine depends mostly on attributes of the unit itself, each unit having things like strength, number of attacks, hit points, etc. But then, it will probably start complicating things with exceptions. It might have special rules for when ranged units enter hand to hand, like their attack strength is reduced by two. Then it might have special rules for orcs, like orcs doubling their strength when charging. Then it might have special rules for orc ranged units, that different from normal ranged units, are able to charge, but they don't get the doubled strength effect. Then it might have special rules for spellcaster, which might happen to be an orc ranged unit as well.
My point here is that rules, as we think of them, don't depend so much on fixed types, but usually depends on predicates. Predicates are simply something that can be said about game elements. For example, I might say an element x is an orc, or a ranged unit or a spellcaster, or all of these, or even none. Each is verifiable in some way, though this verification may rely on axioms. For example, the orc predicate might be based on an axiom (elements either are or are not orcs), while the ranged unit predicate might be based on checking to see if the unit has any attack with range greater than one. Based on these predicates, we define the basic behavior and exceptions of this basic behavior. So, my idea is to make a computer language that follows this paradigm, so that programming in it is more natural and debugging it is easier.
The basic idea is, then, that we have these various "typeless" objects. These objects already have the inherent capacity to store fields in a key->value mapping way. So, a given object might store in the key "name" the value "Albert", and then, if asked the value of the field "name", it would answer "Albert". Also, let's assume we have a few basic objects from out of the system that provide us with functionalities such as numbers, arrays and strings (this isn't really necessary, but will make the idea easier to follow).
Besides these objects, we also have "predicates". Predicates are special in that they are able to check if an object is part of it. For example, the predicate "orc(x)" might tell if an object x is an orc, possibly by checking if the field "race" of x is set to "orc". Predicates aren't limited to one object, either. A predicate enemy(x,y) might tell if x is an enemy of y (which might be important in a wargame when determining issues such as friendly fire, etc). Predicates are also semi-ordered, meaning that some more specific predicates have greater precedence than less specific ones. For example, we might have "orc" as a predicate and "black orc" as a more specific predicate, so black orc is "greater" than orc, but another predicate, like "spellcaster" might not be comparable to either "orc" or "black orc". By the way, if we have two predicates, p and q, and q is greater than p, then if q(x) is true, then p(x) is also true.
Also, we have methods. Methods are associated with four things. First, methods have a name, such as attack, or defend, or getStrength. Second, methods are associated to a cardinality, which is the number of arguments it receives. For example, a method like "defend" might receive only one argument, so that defend(x) means that the object x will try to defend itself, while "attack" might receive two, attack(x,y) meaning that x is attacking y.
Third, each method is associated with a "body", a description of some kind of how to run the method. A method called "getStrength(x)" might simply return the value of the "strength" property of x, while a method getStrengthBonus(x), might take the value given by getStrength(x), divide it by two, subtract five and return the total. Finally (and most interesting), each method is associated with a predicate of equal cardinality. This association means that the method is available to all objects where the predicate hods true. For example, a method like "getSpellPoints(x)" might be associated with a "spellcaster(x)" predicate, so only spellcasters have that method available, whereas "attack(x,y)" might be associated with "enemy(x,y)", so it is only legal for x to attack y if he is an enemy of y. There is also polymorphism in the case that a predicate is greater than another. So, in the orc(x) and black_orc(x) predicate examples, if both have methods called "blood_lust(x)", then the method associated with black_orc will be used instead of the more general one if x is a black_orc.
Finally, the last element we add are special cases. Special cases are all associated with one (or, in a simple extension, one or more) methods. Each special case is comprised of one predicate (possibly a composite one) over the method's variables and a "body". The idea behind special cases is that, whenever you are executing a method it is associated to, if its predicate is valid, then its body is executed. The predicate's body may alter or substitute the original body of the method. So, you might have a method, say, damage(x,y), where x is a character and y is a damage (a special object that encapsulates damage amount and type). We might have a special rule for this method with the predicate (undead(x) & fire(y)) (meaning x is an undead and y is fire damage) and the body "damage(x, 2*y). Note that rules are only applied once, so this doesn't end up in an endless loop.
Each method orders its special cases, so they are executed in the desired sequence (useful when, for example, a few cases add to values whereas others multiply them). Also, one special case may override another, meaning that if its predicate is true, the overridden one isn't checked or executed. Any and all special cases that apply for a particular method are executed in the defined order. So, for example, in the getStrength(x) method, the special case {ghost(x) :- return 0} might override all other considerations, so if x is a ghost, its strength will always be 0.
Finally, we may determine that predicates are able to check the execution stack. This is useful for creating rules like "the strength of orc units double when charging". Simply implement a special case for getStrength(x) whose predicate checks to see if charge(x,y) is further up in the stack. The predicate might be something like charging_orcs(x, y), and it checks if there is a method call charge(x,y) with x being orcs in the stack.
Well, this is it. I am sure I m forgetting something, but I need to sleep now and there is no point in posting every detail in one go. I hope you people forgive my poor communication skills. If anything in the post is unintelligible, ask about it and I will be happy to try to clarify. My objective with this is creating a language where it is easy to create and modify rule sets for games on the fly, where it is easy to test different rule designs before one sets for something definitive or even to allow the rule set to evolve as one develops new games in the same engine.