Tuesday, 13 November 2018

Fighting Fantasy Combat

This is a project inspired by combining two previous posts, namely the introduction of objects and classes, and the creation of a simple adventure game.

In the Fighting Fantasy gamebooks that inspired the latter there is a simple combat system done using two dice, pencil and paper. Each opponent has a Skill score (typically 4-8) and a Stamina score - Stamina is variable and can decrease with damage. When a combatant has 0 stamina they are dead. When combat ensues:
  • Each combatant rolls 2x 6-sided dice and adds the result to their skill score. 
  • If the total scores for each combatant are equal that round is a draw and no damage is inflicted. 
  • If one combatant has a higher total score than the other, then that combatant inflicts 2 stamina damage on the opponent. 
  • This continues until one combatant reaches 0 stamina or below and dies.   
This is a very simple system and I have added to it. Each combatant now has a damage score. This is not the exact amount inflicted but the maximum in a range of integers.
Also each combatant has an armor score (usually 0, 1 or 2) which mitigates damage received from opponents. Thus if a combatant wins a round, damage inflicted on an opponent is between 1 and damage score - opponent's armor score. 
So that's how it works with pencil, paper and dice.
Just to keep things interesting, each combatant may have a treasure score and also may have some items of equipment. When they are defeated, the treasure and equipment are added to the opponent's treasure and equipment (so combatants kill each other then take their stuff).
In terms of Python it might have been doable as lists but this seems to be a good opportunity for object-oriented programming. I've been a bit lazy when it comes to data storage - the data for each combatant is stored within the Python code. This might not be ideal (certainly telling Python to write new data is not feasible) but at this level of simplicity I can get away with it. This program is not purely OOP - it seemed convenient to have a mix of OOP and functional programming, and Python allows me to do that.
Here is the program itself:

import random
import sys

class monster:
    def __init__(self, name, skill, stamina, armor, dam, treasure, equip): # You need to include self
        self.name = name
        self.skill = skill
        self.stamina = stamina
        self.armor = armor
        self.dam = dam
        self.treasure = treasure
        self.status = 'Full Health'
        self.equip = equip
    def display(self): # Again you need to include self
        print("Name    : ", self.name)#Attributes of objects need to say what object they belong to
        print("Skill   : ", str(self.skill))
        print("Stamina : ", str(self.stamina))
        print("Armor   : ", str(self.armor))
        print("Damage  : ", str(self.dam))
        print("Treasure: ", str(self.treasure))
        equipstring = ', '.join(self.equip)
        print("Equip   : ", equipstring)
    def attackroll(self):
        dice1 = int(random.randint(1, 6))
        dice2 = int(random.randint(1, 6))
        #print(self.name, "rolls", dice1, dice2)
        totalattack = dice1 + dice2 + self.skill
        attackstring =  self.name + ' rolls '+ str(dice1) +', '+str(dice2)+ ' + ' + str(self.skill) + ' skill = ' + str(totalattack)
        return totalattack, attackstring
    def dealdamage(self):
        rolldam = random.randint(1, self.dam)
        print(self.name, 'deals', rolldam, 'damage')
        return rolldam
    def damaged(self, dealt):
        dealt = dealt - self.armor
        if dealt < 0: dealt = 0
        self.stamina = self.stamina - dealt
        if self.stamina < 1:
            self.status = "is dead"
        else:
            self.status = "has "+str(self.stamina)+ " Stamina left"
    def picksitems(self, itemlist):
        self.equip += itemlist
        print(self.name, 'has picked up', str(itemlist))
        equipstring = (', '.join(self.equip))
        print(self.name, 'equipment is', equipstring)
    def picksmoney(self, money):
        self.treasure += money
        print(self.name, 'has picked up', str(money))
        print(self.name, 'now has treasure: ', str(self.treasure))
    def dropsitems(self):
        droplist = []
        for i in self.equip:
            droplist.append(i)
        self.equip = []
        print(self.name, 'drops ', str(droplist))
        return droplist
    def dropsmoney(self):
        dropcash = self.treasure
        self.treasure = 0
        print(self.name, 'drops', str(dropcash))
        return dropcash
   

m1 = monster("Orc", 5, 5, 1, 4, 3, ['shortsword', 'shield'])
m2 = monster("Minotaur", 8, 8, 1, 6, 12, ['battleaxe'])
m3 = monster("Giant Spider", 6, 4, 0, 3, 0, [])
m4 = monster("Werewolf", 8, 7, 1, 5, 10, [])
m5 = monster("Dragon", 9, 15, 2, 8, 100, [])
m6 = monster("Dwarf", 7, 7, 2, 4, 10, ['chainmail', 'shortsword'])
m7 = monster("Elf", 8, 5, 1, 6, 10, ['leatherarmour', 'longsword'])
m8 = monster("Killer Weasel", 6, 4, 1, 5, 0, [])
m9 = monster("Assassin", 8, 6, 1, 6, 10, ['leatherarmour', 'scimitar'])
m10 = monster("Giant Rat", 6, 4, 0, 4, 0, [])

monlist = [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10]
for mon in monlist:
    print(monlist.index(mon), mon.name)
disp1 = False
choice = input("Which monster is first combatant (enter number)? ")
try:
    choice = int(choice)
    fighter1 = monlist[choice]
    disp1 = True
    fighter1.display()
except:
    print("Sorry, that monster is not found")

choice = input("Which monster is second combatant? ")
disp2 = False
try:
    choice = int(choice)
    fighter2 = monlist[choice]
    disp2 = True
    fighter2.display()
except:
    print("Sorry, that monster is not found")

if not(disp1) or not(disp2):
    print("disp1 =" +str(disp1) + ", disp2 = "+str(disp2))
    print("Not enough combatants")
    exit
else:
    print("You have chosen "+ fighter1.name +" and "+fighter2.name)

fightcont = True
r = 1
while fightcont == True:
    print("Round", r)
    r += 1
    F1roll, f1string = fighter1.attackroll()
    F2roll, f2string = fighter2.attackroll()
    print(f1string)
    print(f2string)
    if F1roll > F2roll:
        dealt1 = fighter1.dealdamage()
        result = fighter2.damaged(dealt1)
    elif F2roll > F1roll:
        dealt2 = fighter2.dealdamage()
        result = fighter1.damaged(dealt2)
    else:
        print("That round was a draw")
    print(fighter1.name, fighter1.status)
    print(fighter2.name, fighter2.status)
    if fighter1.status == "is dead":
        fightcont = False
        winner = fighter2
        loser = fighter1
    elif fighter2.status == "is dead":
        fightcont = False
        winner = fighter1
        loser = fighter2

print ("Winner is: " + winner.name)
winner.picksmoney(loser.dropsmoney())
winner.picksitems(loser.dropsitems())

Yes it is big (as far as programs on this blog go). But it works, and here are typical results:
==== RESTART: C:\Users\pc\Documents\Programming\fightingfantasy_combat.py ====
0 Orc
1 Minotaur
2 Giant Spider
3 Werewolf
4 Dragon
5 Dwarf
6 Elf
7 Killer Weasel
8 Assassin
9 Giant Rat
Which monster is first combatant (enter number)? 1
Name    :  Minotaur
Skill   :  8
Stamina :  8
Armor   :  1
Damage  :  6
Treasure:  12
Equip   :  battleaxe
Which monster is second combatant? 8
Name    :  Assassin
Skill   :  8
Stamina :  6
Armor   :  1
Damage  :  6
Treasure:  10
Equip   :  leatherarmour, scimitar
You have chosen Minotaur and Assassin
Round 1
Minotaur rolls 1, 1 + 8 skill = 10
Assassin rolls 3, 2 + 8 skill = 13
Assassin deals 1 damage
Minotaur has 8 Stamina left
Assassin Full Health
Round 2
Minotaur rolls 4, 6 + 8 skill = 18
Assassin rolls 2, 5 + 8 skill = 15
Minotaur deals 2 damage
Minotaur has 8 Stamina left
Assassin has 5 Stamina left
Round 3
Minotaur rolls 2, 1 + 8 skill = 11
Assassin rolls 6, 5 + 8 skill = 19
Assassin deals 3 damage
Minotaur has 6 Stamina left
Assassin has 5 Stamina left
Round 4
Minotaur rolls 3, 2 + 8 skill = 13
Assassin rolls 6, 6 + 8 skill = 20
Assassin deals 3 damage
Minotaur has 4 Stamina left
Assassin has 5 Stamina left
Round 5
Minotaur rolls 5, 2 + 8 skill = 15
Assassin rolls 5, 2 + 8 skill = 15
That round was a draw
Minotaur has 4 Stamina left
Assassin has 5 Stamina left
Round 6
Minotaur rolls 3, 3 + 8 skill = 14
Assassin rolls 5, 5 + 8 skill = 18
Assassin deals 4 damage
Minotaur has 1 Stamina left
Assassin has 5 Stamina left
Round 7
Minotaur rolls 5, 1 + 8 skill = 14
Assassin rolls 6, 5 + 8 skill = 19
Assassin deals 2 damage
Minotaur is dead
Assassin has 5 Stamina left
Winner is: Assassin
Minotaur drops 12
Assassin has picked up 12
Assassin now has treasure:  22
Minotaur drops  ['battleaxe']
Assassin has picked up ['battleaxe']
Assassin equipment is leatherarmour, scimitar, battleaxe
>>>


Things to do to improve this program:
  • Equipment could influence combat stats, especially for humanoid creatures that can use it (a dragon would have no use for chainmail armour but an elf might). This may involve an item class and objects. 
  • Currently if the same creature is selected twice, there is just one object that fights itself (a rather bizarre situation). It would be better if a duplicate object could be created with a different name but same combat stats. 
  • Currently no creatures can used ranged/missile weapons (bows & arrows, magic spells, dragon's fiery breath). Perhaps those creatures with ranged attacks get a free attack before regular hand-to-hand combat ensues. 
  • As noted above, storing the data for each combatant could be done better, either in a CSV file or an SQLite database. 
  • The grand project is to combine this combat system with the adventuring exploration system shown in the post about a simple adventure program to more closely replicate the original Fighting Fantasy books.  
  • In that case, items may be used to deal with non-combat situations (such as using a picked-up torch to light a darkened room, thereby revealing a dangerous pit).
  • It would also involve creating a Player-Character object representing the player interacting with the fantasy world.   



No comments:

Post a Comment