This tutorial will introduce you to the basics of working with OCF.
For this tutorial, I will be making a simple adventure game. The player takes the role of the intrepid Captain Limelight. Captain Limelight must use his lazer blaster to keep away the aliens, who are trying to kill him.
Before starting, you will, of course, want to make sure that OpenCombatFlow is installed. If you haven't already, run this from the command line:
pip install opencombatflow
Create a new python file in the directory that you want your game. You can call yours whatever you'd like. I am going to call mine "limelight.py".
Next, you'll want to import the character module from opencombatflow. This will give you access to the Character class, and the combat handler.
from opencombatflow import character as c
The time has come to create your first character! Yay! We start on the line following our import...
class hero(c.Character): name = "Limelight" HP = 30 groups = ['good']
To create the "hero" character, we extend the class Character from the module character. This new class can be called whatever you like.
"name", "groups", and "HP" are built in attributes of the Character class that do specific things. HP is the amount of life the character has, name is the character's name, and groups is a list of groups that the character is a part of. (This will make targetting easier).
You might notice that we haven't yet defined a lot for the hero to do. We must create two new methods for all characters: getActionBlock and getReactionBlock. These allow the combat handler to automatically handle events
class hero(c.Character): name = "Limelight" HP = 30 groups = ['good'] def getActionBlock(self): return {} def getReactionBlock(self, action): return {}
Notice that we are returning empty dictionaries. OCF uses a system of structured dictionaries to pass information between itself and the user. There are multiple types, and each type has it's own rules. VIew them all here.
For now, let's focus on getActionBlock. If we check the above link, we see that there are three required fields of an action block: "range", "user", and "name". Let's go ahead and create the minimal structure for the action that we will return.
ch = c.combatHandler() class hero(c.Character): name = "Limelight" HP = 30 groups = ['good'] def getActionBlock(self): global ch action = { 'user': self, 'name': "Blaster Shot", 'range': {"character":ch.getRandomCharacterInRange({"group":"bad"})}, } pass def getReactionBlock(self, action): pass
You might be confused as to what I did with the "range" field. Notice that, between the previous step and this step, I created a combatHandler object called "ch". The combatHandler stores information about all characters in combat. So, I used the combatHandler member function getRandomCharacterInRange to get a random bad guy, and chose that bad guy as the target of the attack. I relied heavily on rangeBlocks, you can read about them here.
Okay, let's do some damage.
def getActionBlock(self): global ch action = { 'user': self, 'name': "Blaster Shot", 'range': {"character":ch.getRandomCharacterInRange({"group":"bad"})}, 'damage': { 'energy': 10 } } pass
Notice that the DSD allows us to specify any type of damage we'd like. We could have used "wkdmfn" damage, and it would have worked. That is, 'energy' is not a 'special' keyword within OCF.
Now, we just return the created action.
def getActionBlock(self): global ch action = { 'user': self, 'name': "Blaster Shot", 'range': {"character":ch.getRandomCharacterInRange({"group":"bad"})}, 'damage': { 'energy': 10 } } return action
Because we are lazy, let's go ahead and implement the minimal getReactionBlock. We can come back later and add a better reaction if we'd like.
def getReactionBlock(self, action): reaction = {'user': self} return reaction
The full code now looks like this:
from opencombatflow import character as c ch = c.combatHandler() class hero(c.Character): name = "Limelight" HP = 30 groups = ['good'] def getActionBlock(self): global ch action = { 'user': self, 'name': "Blaster Shot", 'range': {"character":ch.getRandomCharacterInRange({"group":"bad"})}, 'damage': { 'energy': 10 } } return action def getReactionBlock(self, action): reaction = {'user': self} return reaction
I'm going to go ahead and skip past making the alien character (which is essentially the same way), so we can get to combat.
class alien(c.Character): name = "Alien" HP = 10 groups = ['bad'] def getActionBlock(self): global ch action = { 'user': self, 'name': "Slime Slap", 'range': {"character":ch.getRandomCharacterInRange({"group":"good"})}, 'damage': { 'toxic': 3 } } return action def getReactionBlock(self, action): return {'user': self}
Now, at long last, we can implement combat (That's why you're here, right?). Before combat begins, we must add characters to the combat handler.
ch.addCharacter(hero()) ch.addCharacter(alien()) ch.addCharacter(alien()) ch.addCharacter(alien())
In the above lines, we add four Character objects to the combat handler.
Now, let's implement the game loop.
while True: ch.turn()
That's ... it? Well, sort of. This is the minimum required setup. If we run it, however, we get this error:
KeyError: "The element None is of type <class 'NoneType'>, not of required type <class 'opencombatflow.character.Character'>. (Evaluating {'character': None})"
There must be an error in our code, right? Wrong. Well, mostly wrong. What actually happened is that our code played the entire game quickly and invisably, and then ran out of characters to attack. (Which is why it is complaining about recieving a None instead of a Character).
Let's enclose the turn() statement in a try/except block. (This isn't good practice, but will work until we implement a proper game-over state)
while True: try: ch.turn() except: print("Game Over.")
Now, when we run it, it looks like this:
Game Over.
That's all, thanks for reading!
Just kidding! Let's add some messages to the screen so we can figure out what the heck is happening. A log of everything that occurs in game is stored in the combat handler, in the log block format as defined in the DSD.
while True: try: ch.turn() except: print("Game Over.") break for i in ch.getLog(): print(i) ch.flushLog() #This clears the log for next time.
The output now looks like this:
{'messageType': 'startOfTurn', 'character': <__main__.hero object at 0x00BF44B0>} {'messageType': 'action', 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFF6B0>}, 'damage': {'energy': 10}}} {'messageType': 'reaction', 'reaction': {'user': <__main__.alien object at 0x00BFF6B0>}, 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFF6B0>}, 'damage': {'energy': 10}}} {'messageType': 'attackHit', 'damage': {'damageTaken': 10}, 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFF6B0>}, 'damage': {'energy': 10}}} {'messageType': 'death', 'character': <__main__.alien object at 0x00BFF6B0>} {'messageType': 'startOfTurn', 'character': <__main__.alien object at 0x00BFF670>} {'messageType': 'action', 'action': {'user': <__main__.alien object at 0x00BFF670>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'reaction', 'reaction': {'user': <__main__.hero object at 0x00BF44B0>}, 'action': {'user': <__main__.alien object at 0x00BFF670>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'attackHit', 'damage': {'damageTaken': 3}, 'action': {'user': <__main__.alien object at 0x00BFF670>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'startOfTurn', 'character': <__main__.alien object at 0x00BFFED0>} {'messageType': 'action', 'action': {'user': <__main__.alien object at 0x00BFFED0>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'reaction', 'reaction': {'user': <__main__.hero object at 0x00BF44B0>}, 'action': {'user': <__main__.alien object at 0x00BFFED0>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'attackHit', 'damage': {'damageTaken': 3}, 'action': {'user': <__main__.alien object at 0x00BFFED0>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'startOfTurn', 'character': <__main__.hero object at 0x00BF44B0>} {'messageType': 'action', 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFF670>}, 'damage': {'energy': 10}}} {'messageType': 'reaction', 'reaction': {'user': <__main__.alien object at 0x00BFF670>}, 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFF670>}, 'damage': {'energy': 10}}} {'messageType': 'attackHit', 'damage': {'damageTaken': 10}, 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFF670>}, 'damage': {'energy': 10}}} {'messageType': 'death', 'character': <__main__.alien object at 0x00BFF670>} {'messageType': 'startOfTurn', 'character': <__main__.alien object at 0x00BFFED0>} {'messageType': 'action', 'action': {'user': <__main__.alien object at 0x00BFFED0>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'reaction', 'reaction': {'user': <__main__.hero object at 0x00BF44B0>}, 'action': {'user': <__main__.alien object at 0x00BFFED0>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'attackHit', 'damage': {'damageTaken': 3}, 'action': {'user': <__main__.alien object at 0x00BFFED0>, 'name': 'Slime Slap', 'range': {'character': <__main__.hero object at 0x00BF44B0>}, 'damage': {'toxic': 3}}} {'messageType': 'startOfTurn', 'character': <__main__.hero object at 0x00BF44B0>} {'messageType': 'action', 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFFED0>}, 'damage': {'energy': 10}}} {'messageType': 'reaction', 'reaction': {'user': <__main__.alien object at 0x00BFFED0>}, 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFFED0>}, 'damage': {'energy': 10}}} {'messageType': 'attackHit', 'damage': {'damageTaken': 10}, 'action': {'user': <__main__.hero object at 0x00BF44B0>, 'name': 'Blaster Shot', 'range': {'character': <__main__.alien object at 0x00BFFED0>}, 'damage': {'energy': 10}}} {'messageType': 'death', 'character': <__main__.alien object at 0x00BFFED0>} Game Over.
Uh...okay. Let's translate this into english in the main game loop.
while True: try: ch.turn() except: print("Game Over.") break for i in ch.getLog(): if i['messageType'] == 'startOfTurn': print(f"{i['character'].name} starts his turn.") elif i['messageType'] == 'death': print(f"{i['character'].name} dies...") elif i['messageType'] == 'action': print(f"{i['action']['user'].name} uses {i['action']['name']}") elif i['messageType'] == 'attackHit': print(f"{i['action']['user'].name}'s {i['action']['name']} hits for {i['damage']['damageTaken']} damage") ch.flushLog()
This outputs the following:
Limelight starts his turn. Limelight uses Blaster Shot Limelight's Blaster Shot hits for 10 damage Alien dies... Alien starts his turn. Alien uses Slime Slap Alien's Slime Slap hits for 3 damage Alien starts his turn. Alien uses Slime Slap Alien's Slime Slap hits for 3 damage Limelight starts his turn. Limelight uses Blaster Shot Limelight's Blaster Shot hits for 10 damage Alien dies... Alien starts his turn. Alien uses Slime Slap Alien's Slime Slap hits for 3 damage Limelight starts his turn. Limelight uses Blaster Shot Limelight's Blaster Shot hits for 10 damage Alien dies... Game Over.
Pretty cool, eh? Well, not really, but you can probably see how this could be extended. For esxample, more logic could be added to the player class's getActionBlock method, including gathering input from the player. Alternatively, better AI could be written for our enemies.
Back to Documentation