SlugFest
This tutorial quickly lays out the necessary bits for using Combat.py along with StackLang.py to simulate role-playing style combat. The ultimate reason for my writing these two modules is my ongoing PBM system. Ideally Swarming with Dwarves will allow players to write custom behavior patterns for their dwarfs, as well as create customize combat scenarios.
Slugfest is fairly simple, and much of what I wrote into Slugfest has been incorporated into Combat.py. The whole point is to automatically run a combat simulation that can be programmed. This is similar to Robowar, a fun little game for the Macintosh. The big difference it that SlugFest (and anything derived from Combat) is turn-based, not a real-time simulator.
The Big Picture
The Combat module has five components. Attacks, Combatants, Orderers, Damagers, and Managers. Combatants use Attacks, Managers use Orderers, Damagers, to work with Combatants. For a full treatment of these classes, see the Combat.py documentation .
Defining Attacks
The first thing to do is to invent some attacks. SlugFest is not designed to be pretty, or even elegant. In order to use the Attack class, create a subclass:
class SlugfestAttack(Combat.Attack):
DamageTypes = Combat.enum('Crushing','Stabbing','Grappling','Slapping')
EffectTypes = Combat.enum('hurt','stumble','kochance')
ElementTypes = Combat.enum('normal','fire','acid','magic')
def __init__(self,name,**kw):
Combat.Attack.__init__(self,name,**kw)
The three class variables, DamageTypes , EffectTypes , and ElementTypes are enumerated types. Python does not have an enumerated types, but I use a variation from one I found on the web. It’s entirely possible that this is overkill and a simple list would do. The first item in each type is the default argument, so if you don’t declare a damage type, effect type, or element type when creating instances of the Attack class they will be ‘Crushing’, ‘hurt’, and ‘normal’, respecively.
It is wise to create a dictonary (or some other notation) of what each effect is supposed to do:
effectdict = {
'hurt':'The Normal attack. Nothing special happens',
'stumble':'The attack prevents the opponent from atting in the next round',
'kochance':'The attack has a chance of knocking the opponent out'
}
Now we can create attacks for our combatants to use:
NoAttack = SlugfestAttack('No attack',bdr='d0')
Slap = SlugfestAttack('Slap',bdr='d2',bdt='Slapping')
Jab = SlugfestAttack('Jab',bdr='d2+1',bdt='Crushing')
Eyegouge = SlugfestAttack('Eye Gouge',bdr='d1',bdt='Stabbing',effect='stumble')
Elbow = SlugfestAttack('Elbow Jab',bdr='d2+2',bdt='Crushing')
Hook = SlugfestAttack('Hook',bdr='d4',bdt='Crushing')
Uppercut = SlugfestAttack('Uppercut',bdr='d4+1',bdt='Crushing',effect='kochance')
The first item when calling the Attack class (or in this case the sub-class SlugFestAttack ) is the name of the attack. This is required. Everything else comes in key=value pairs. You can use either basedamageroll or its abbreviation bdr for keys. This shortcut makes it a bit easier to use. Attacks can have any of seven customized attributes:
- basedamageroll or bdr must be a DieRoll formatted string. If you’re not familiar with role playing games, ‘3d6+1’ means ‘three six sided dice, add one.' DieRoll is another program of mine that manages these dice rolls automatically.
- basedamagetype or bdt must be a string object listed in the class’ DamageType type. These are intended to represent physical types of damage. In Dungeons and Dragons the types are ‘bludgeoning’, ‘piercing’, and ‘slashing.'
- basedamageelement or bde must be a string object listed in the class’ ElementType . These are intended to cover other types of damage, magical, lightning, fire, water, acid, poison, etc.
- specialdamageroll or sdr follows the same restrictions as basedamageroll .
- specialdamagetype or sdt follows the same restrictions as basedamagetype .
- specialdamageelement or sde follows the same restrictions as basedamageelement .
- effect must be in the class’ EffectType type. The combat manager can check for these effects as it processes combat.
With these attacks, we can declare the language combatants will use
The SlugFestLanguage
This part of the tutorial has nothing to do with Combat. StackLanguage is a small programming language based on reverse polish notation and last-in-first-out stacks. So, briefly:
class SlugFestLanguage(StackLang.StackLanguage): def __init__(self): StackLang.StackLanguage.__init__(self,'SlugFest',0.1) self.ImportLibrary(StackLang.FiveFunctionLibrary) self.ImportLibrary(StackLang.ComparisonsLibrary) self.ImportLibrary(StackLang.ControlOperationsLibrary) self.AddRule(['random'],'do_random') def do_random(self,caller): """adds a random number onto the stack""" from random import randint self.Stack.push(str(randint(0,99))) sf = SlugFestLanguage() sf.AllowNewTokens = 1
The StackLanguage class uses the ImportLibrary method to add three predefined libraries to it’s capabilites, and then defines one more function: do_random. All the do_random function does is push a number between 0 and 99 inclusive onto the stack. This will be used to make decisions later on.
The sf.AllowNewTokens = 1 declaration is important here. Otherwise the current version of the stack language processor could stop cold. For more information about these libraries, see the StackLanguage Sample Libraries page.
Now we write a simple program in the SlugFestLanguage:
sluggertext = """ random r' r 10 lt slap if r 25 lt jab if r 50 lt hook if r 75 lt elbow if r 95 lt uppercut eyegouge ife slap: 0 end jab: 1 end hook: 2 end uppercut: 3 end elbow: 4 end eyegouge: 5 end """
Real quick StackLanguage tutorial: Case doesn’t matter. It only handles integers. Anything that ends in a colon is a label and it can be the only thing on the line. Comments are from a # sign to the end of the line. Writing to a register is done with the apostrophe.
Slightly longer tutorial: The first instruction is the keyword “random”. This pushes an integer between 0 and 100 onto the stack. It stores this in the register “r”. After the second instruction ( r’ ) the stack is empty. The second line recalls r and places the value of r onto the stack. Then the number 10 is pushed onto the stack. The lt stands for less than. If r is less than 10, then a 1 is pushed on the stack (the r and the 10 are removed from the stack), otherwise it pushes 0 onto the stack. The slap is pushed onto the stack. Actually, an instruction number (28) is pushed onto the stack. The if looks at the 1 or 0 left by lt . If it finds a 1 in jumps to the instruction at the slap label, otherwise it moves on.
And on it goes. The purpose of this program is to determine what kind of attack to use each turn. The end instruction is just that; it stops processing and the top of the stack is a number from 0 to 5.
Creating the Slugger class
Now we have methods of attack, and a way to decide which attack to use each turn. The Slugger class controls the rest of the combatant:
class Slugger(Combat.Combatant):
"""Slugger is the basic combatant for SlugFest
"""
def __init__(self,name,**kw):
Combat.Combatant.__init__(self,name)
self.Name = name
self.Attacks = []
self.Program = sluggertext ## All Sluggers have same program
self.Registers = sf.Registers
self.PermEffects = []
self.TempEffects = []
self.strength = 2
self.init = 1
for k,v in kw.items():
if k == 'attacks':
for item in v:
self.AddAttack(item)
def AddAttack(self,item):
self.Attacks.append(item)
def NewProgram(self,odata):
self.Program = text
def attack(self,odata={}):
### Determine what attack to use
### Let the SFProgram run, the top of the stack is the
### index of the Attacks list to use
data = {'life':self.life, 'class':self.__class__.__name__,
'dtaken':self.maxlife-self.life}
for k in data.keys(): sf.Registers[k.upper()] = data[k]
for k in odata.keys(): sf.Registers[k.upper()] = odata[k]
sf.Feed(self.Program)
sf.Reset()
sf.Run()
res = int(sf.Stack.top())
if res < len(self.Attacks):
return self.Attacks[res]
else:
return self.Attacks[0]
def attackstring(self):
### Limit the string to mention the attack.
return '%s attacks with %s for %d damage'
Look at the __init__ function. The custom items here are the Program and Register attributes. The init attribute is also a customized attribute. The Combatant class has an attribute speed but I decided not to use it. The Combatant class as a method attackstring() which normally picks a random quote from the hitcomments attribute, which is a list. Here I want to state what was thrown, so I use formatting string which are unresolved here. It will be important to resolve them later.
The attack method is also customized from the normal attack method. It is passed a dictionary generated by the Combat Manager that tells the combatant basic information about the opponent it happens to be facing. It also updates information about itself that the SlugFestLanguage instance can use. Here’s where we put things in the StackLanguage register that weren’t there at compile time, and why it was important to set AllowNewTokens to 1.
Currently the attack method recompiles the text of the custom program, compiles it, then runs it. This is wasteful overhead which should be reduced in future versions. It also checks that result of the program is valid, otherwise it uses the first Attack in the Attacks list.
Making a customized Damage manager
Now we can make a customized Damage Manager. The purpose of the Damage Manager is to get the attack objects from the combatants and the total attack generated by the combatant. It then processes those attacks, then distributes the damage to the combatants.
class SlugFestDamageManager(Combat.BaseDamageManager):
def __init__(self,cm):
Combat.BaseDamageManager.__init__(self,cm)
def processeffects(self,x,xAt,dam,y):
"""x attacker
xAt = attackers attack object
dam = attackers damage
y - defender
"""
res = 0
if xAt.Effect == 'kochance':
kocheck = Combat.rolldice('d20')+dam
if kocheck > y.life:
y.life = 0
self.addlog('Knockout!')
res = 1
elif xAt.Effect == 'stumble':
stcheck = Combat.rolldice('d20')
if stcheck > 10:
y.TempEffects.append('stumble')
self.addlog('%s reels from the blow' % y.Name)
res = 1
else:
self.addlog('%s dodges the nasty eye gouging' % y.Name)
return res
def getAttack(self,x,y):
"""checks for temporary effects.Returns an attack"""
if 'stumble' in x.TempEffects:
x.TempEffects.remove('stumble')
return NoAttack
else:
### create the odata dictionary. This is the defenders
### information, preface everthing with an 'o'
odata = { 'olife': y.life }
return x.attack(odata)
def processdamage(self,combatants):
a,b = combatants
aAttack = self.getAttack(a,b)
bAttack = self.getAttack(b,a)
aTotal = aAttack.generateDamage()
bTotal = bAttack.generateDamage()
self.addlog(a.attackstring() % (a.name,aAttack.Name,aTotal))
### Find any effects of the attack
if not self.processeffects(a,aAttack,aTotal,b): b.takedamage(aTotal)
if b.isdead():
self.addlog('%s falls.' % b.name)
else:
self.addlog(b.attackstring() % (b.name,bAttack.Name,bTotal))
if not self.processeffects(b,bAttack,bTotal,a): a.takedamage(bTotal)
if a.isdead():
self.addlog('%s falls.' % a.name)
self.addlog("%s: %d and %s: %d" % (a.name,a.life,b.name,b.life))
The processdamage is the method that the Combat Manager will call. Here I’ve declared two more methods, processeffects and getAttack . These helpers seem cumbersome, but I would rather add a few strange calls than have to type a procedure twice (and debug the thing twice).
Future fixes will solve a basic problem with this set up. Right now the two lines aTotal = aAttack.generateDamage() and bTotal = bAttack.generateDamage() rely on the Attack instance to create the damage. Whatever additions the Combatant may add to this needs to be addressed. Combatants have a basic generateDamage method, but currently it is not used to this purpose. The processeffects method handles effects attached to the Attack instance. This will become a very long sequence of if..elif...else as more Effects are thought up. The getAttack method checks the combatatns temporary effects (it should probably also check the combatants PermEffects code) and determines if it alters the Attack in any way.
It is also possible to customize the Orderer, which determines the order in which the combatants attack. I haven’t subclassed this, but it is possible to do and include effects. The stumble effect could just as easily deduct points from the Initiative roll (much like D&D, higher initiative goes first with the InitOrderer class).
Setting up the Manager
The last thing we need to do is set up the Combat Manager. We need to make two combatants first:
NormalAttacks=[Slap,Jab,Hook,Uppercut,Elbow,Eyegouge]
Harry=Slugger('Harry',attacks=NormalAttacks)
Sally = Slugger('Sally',attacks=NormalAttacks)
### Set up the Combat Manager
SlugFestCombatManager = Combat.CombatManager(Harry,Sally)
SlugFestCombatManager.setOrderer(Combat.InitOrder,roll='d20',att='init')
SlugFestCombatManager.setDamageManager(SlugFestDamageManager)
### FIGHT!
print '--'*13
SlugFestCombatManager.processcombat()
That’s it. The NormalAttack list makes it a bit easier to create equal Sluggers. Creating the CombatManager requires a list of Combatants. Right now it can only handle two, but it doesn’t do any error checking (my fault). The setOrderer method uses one of the basic orderes and passes along the custom attributes. The setDamagemanger tells the Combat Manager to use the customized one we wrote earlier.
Tweaking and customizing
The first small thing you may notice after all of this, the Sluggers program chooses a random attack. We can get a more “rational” decision making process by having each combatant determine if they have less life than their opponent, and if so, use the Uppercut attack, otherwise, choose another attack. We can also have them decide to use the Eyegouge if they’ve taken more than 15 points of damage (or their own life is less than 5):
sluggertext2 = """ olife life gt uppercut if dtaken 15 gt eyegouge if random r' r 50 lt jab hook ife slap: 0 end jab: 1 end hook: 2 end uppercut: 3 end elbow: 4 end eyegouge: 5 end """
To use it, either replace the self.Program in the Slugger class, or:
Harry.Program= sluggertext2
This will let Harry use the new program, and Sally will use the old program.