Saturday, 17 November 2018

A Fighting Fantasy Combat Tournament and the continue keyword

So I have been working a bit more on the Fighting Fantasy combat thing.  The latest version has two major changes.
First of all it has a tournament system for 2, 4, 16 or 32 combatants. The surviving combatants keep loot, treasure and current stamina score (no time to rest up and heal) from one stage to the next. This is made easier by keeping both combatants and items as objects.

Secondly it uses SQLite for storing data about combatants and items. There are two tables in the fight.db database, one for combatants (the monster table) and one for items (equip table). To tell the program which combatants start with which equipment is down to a column in the items' table with the initial owner's name. The program then turns the data from these tables into objects using the monster and item classes.
The monster class (for combatants) starts with the __init__ method which creates new monster objects. You can see that although the straightforward integer and string attributes are passed as arguments to the __init__ method, the equip attribute, which is a list of equipment objects associated with that monster object, refers back to the database to find records in the equip table with the monster object's name, then uses those records to create new equipment objects.  The __init__ method also then takes the adjustments (adjust attribute) from newequip and applies them to the monster's armor or damage attribute according to equiptype.

class monster:
    def __init__(self, name, skill, stamina, treasure): # You need to include self
        self.name = name
        self.skill = skill
        self.stamina = stamina
        self.treasure = treasure
        self.status = 'Full Health'
        self.equip = []
        self.armor = 0
        self.dam = 0
        curs.execute("SELECT * FROM equip WHERE monname = '"+self.name+"';")
        equipdump = curs.fetchall()
        for line in equipdump:
            newequip = equipment(line[0], line[1], line[2], line[3], line[4])
            self.equip.append(newequip)
            if newequip.equiptype == "weapon":
                self.dam += newequip.adjust
            elif newequip.equiptype == "armour":
                self.armor += newequip.adjust


Because entering and editing data straight into SQLite databases is not very user-friendly, I wrote a second program to manage the database - the program that runs the combat and tournament only reads from the database.

One new keyword I've found useful is continue. This is used in loops and is a counterpart to break. Whereas break tells the program to leave the loop and carry on with the code immediately afterwards, continue tells Python to go back to the start of the while loop rather than carrying on with the rest of the indented code.
This is my using continue in context. I want the user to choose how many combatants are in the tournament and there are several criteria for valid input:

while goodselect == False:
    print("There are "+str(len(monlist))+" combatants ready and waiting")
    quantcombat = input("Do you want 2, 4, 8 or 16 combatants? ")
    try:
        quantcombat = int(quantcombat)
        quanttuple = (2, 4, 8, 16)
    except:
        print("Sorry, not a number!")
        continue
    if quantcombat not in quanttuple:
        print("Sorry, not a suitable tournament number")
        continue
    elif quantcombat in quanttuple:
        print("you have selected "+str(quantcombat)+" combatants")
        stages = quanttuple.index(quantcombat)
        goodselect = True


The result is that until the user inputs an integer that is in quanttuple, Python will go back to the top of the while loop.
I have decided not to copy and paste all the code in the program into this blog, as this is getting quite big (217 lines for the tournament program, 144 lines for the database manager).
As well as writing the program, I have done some data entry - currently 20 combatants, each with 1 weapon and 1 armour (either natural or lootable).
One thing I have realised since starting this is that this SQLite database could be used by other programs involving Fighting Fantasy combat, and I believe I may use the same database for a proper adventure that combines the combat and items system here with the area and choosing options system of my previous project.

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.   



Monday, 12 November 2018

Using pip on Windows

This is a slightly self-centred post, as I am doing it primarily for my own reference. As I previously mentioned, pip is the program that allows Python coders to download and install third-party modules. So if you hear about a useful module that doesn't come bundled with Python, you use pip to get it to work.
Okay, this is what's worked for me on my Windows 10 machine.

Open up Windows command line (not IDLE).
CD to where your Python is installed. For me this is
C:\Users\666\AppData\Local\Programs\Python\Python36-32
Then use the command
python -m pip install modulename

for example upgrading the pip module:
C:\Users\666\AppData\Local\Programs\Python\Python36-32>python -m pip install --upgrade pip
Collecting pip
  Downloading https://files.pythonhosted.org/packages/c2/d7/90f34cb0d83a6c5631cf71dfe64cc1054598c843a92b400e55675cc2ac37/pip-18.1-py2.py3-none-any.whl (1.3MB)
    100% |████████████████████████████████| 1.3MB 3.3MB/s
Installing collected packages: pip
  Found existing installation: pip 18.0
    Uninstalling pip-18.0:
      Successfully uninstalled pip-18.0
Successfully installed pip-18.1

C:\Users\666\AppData\Local\Programs\Python\Python36-32>python -m pip install PIL

and again:

C:\Users\666\AppData\Local\Programs\Python\Python36-32>python -m pip install Image
Collecting Image
  Downloading https://files.pythonhosted.org/packages/0c/ec/51969468a8b87f631cc0e60a6bf1e5f6eec8ef3fd2ee45dc760d5a93b82a/image-1.5.27-py2.py3-none-any.whl
Collecting pillow (from Image)
  Downloading https://files.pythonhosted.org/packages/6c/60/4c0e6702a39eab8d5d4d210f283907cbe387fcffeb873d8eb8c3757a21a9/Pillow-5.3.0-cp36-cp36m-win32.whl (1.4MB)
    100% |████████████████████████████████| 1.4MB 2.1MB/s
Collecting django (from Image)
  Downloading https://files.pythonhosted.org/packages/32/ab/22530cc1b2114e6067eece94a333d6c749fa1c56a009f0721e51c181ea53/Django-2.1.2-py3-none-any.whl (7.3MB)
    100% |████████████████████████████████| 7.3MB 2.1MB/s
Collecting pytz (from django->Image)
  Downloading https://files.pythonhosted.org/packages/52/8b/876c5745f617630be90cfb8fafe363c6d7204b176dc707d1805d1e9a0a35/pytz-2018.6-py2.py3-none-any.whl (507kB)
    100% |████████████████████████████████| 512kB 536kB/s
Installing collected packages: pillow, pytz, django, Image
  The script django-admin.exe is installed in 'C:\Users\666\AppData\Local\Programs\Python\Python36-32\Scripts' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed Image-1.5.27 django-2.1.2 pillow-5.3.0 pytz-2018.6

Note that all of this is on the Windows command line, not IDLE. 
I wanted to install the Image module as I wanted to something with images. Pip (or at least the system it talks to) then decided that it was necessary to download and install other modules. One of the things about modules is that they may have dependencies - some require other modules to function. 

Wednesday, 7 November 2018

Random thoughts on CSV files

Why bother with SQLite when I could use CSV files?
Since restarting this blog a month ago all of my posts have involved SQLite to varying degrees. For those who are expecting to just learn about Python this may be an annoying side-trek. One alternative I discussed in an earlier post is using CSV or text files to hold persistent data. Python can open, read from and write to these files. There are a number of advantages that SQLite has.
  • SQLite is faster when file sizes and quantities of data increase. In terms of speed, SQLite is something of a compromise. For enterprise/corporation level databases, something like Oracle or Microsoft SQL Server are in a whole different league of performance and can cope with millions of records and thousands of tables (see this page for MS SQL Server). Nonetheless, SQLite is better than using Python manipulating CSV files, and it is easier for me to install and run than the enterprise-level SQL programs. 
  • SQLite does a lot of work that you would otherwise need to do in Python. This is something I've appreciated even at this relatively low level. One well-written SQLite command passed by the cursor object can replace a dozen lines of Python code with their associated possible errors. As stated before, SQL is a language itself, and brings with it extra capabilities for managing and searching databases, especially relational databases. The effort put into learning the basics of SQL starts to pay off quickly. 
  • SQLite is professionally more useful, mainly because it uses SQL and a lot of businesses use SQL-based programs and databases rather than CSV files. They might use a different SQL program (like those mentioned above) but the SQL is mostly the same.  
  • Editing data in SQLite is far easier than reading/writing to CSV files, particularly if there is one or two items of data to edit in the middle of the table but other data around it does not need to be changed. For SQLite you can use the UPDATE command. For CSV files it is not easy to rewrite a specific line while leaving the rest of the file alone. At my (limited) skill I would end up reading the whole file into Python, making modifications in Python, then overwriting the entire file with the new version held in Python. Although doable with small files, when you are dealing with big files this can become very inefficient. 
Having said all that there are modules that help Python to interact with CSV files, including the csv module documented here. I probably ought to learn these, but right now SQLite seems better, particularly now I've made the initial investment of time and effort in getting SQLite running in my Python programs. CSV files can also be opened and edited in spreadsheets, unlike SQLite files, thus partially nullifying my final bullet point.
And SQLite, being a language with many nice features, can import data from and export data to CSV files, so if you change your mind after entering big tables of data you can always move your data from one to the other.

Thursday, 1 November 2018

Scripts, Functions, Classes and Objects

During my Open University course I was introduced to Object Oriented Programming (OOP). Since then I have not really needed to use it, but Python is a flexible programming language that allows the programmer to create scripts, procedural programs or object-oriented programs.
Scripts are simple programs where the instructions sit in the main program and are not sorted into functions or objects.
Procedural programming is where I encapsulate the instructions into functions (also known in other languages as procedures, hence the term procedural) with particular inputs (arguments) and outputs (returned values).
Object Oriented Programming is like the next step along from using functions, and it bundles the information and instructions into objects, which often represent real things. An object will often have methods (instructions for how the object behaves, basically the same as the functions we have looked at before), and attributes (information about the state of the object, similar to variables but specific to that object).

Classes are the templates that each object of that class will follow. Think of the design for a car, with what it should be capable of and what shape each car should be. The objects would then be all the cars made to that design. They may have different attributes (different number plates, different owners, different colour schemes) but they are all from the same design.

The procedural programming we have done before seems adequate. So why bother with OOP? 
Encapsulation: An object does not need to reveal its inner workings to other objects or functions in the rest of the program.  Instead it interacts through its methods. Other parts of the program will call on that object's methods and the method may return an answer in a particular format. The class should create objects that are as self-contained as possible, with all the methods and attributes it needs to do its job. This means that debugging can often be narrowed down to a faulty object or class.
Inheritance: A class can have subclasses - variations on a theme. This means that a programmer does not have to create the code for a slightly different class from scratch but can say "Take this class, but change this and this". Think of a design for a car (a class) with hatchback or coupe variants (subclasses). For the objects of these subclasses, many of the components and interfaces are the same (the car designers can reuse much of the original design - inherited from the main class) but with some differences.
Closer modelling of real-life situations: The car analogy is an example. Many programs help users deal with things such as hotels (Trivago), items up for auction (eBay) or fantastic monsters (World of Warcraft). Treating these things as objects and classes makes it easier for the programmers to understand how the program should work.


So how about a quick program to show the basics of objects and classes?

class test:
    def __init__(self, num):
        self.number = num
        self.doublenum = num * 2
        self.x = "Hello World"

    def show(self):
        print("First number is: ", self.number)
        print("Second number is: ", self.doublenum)
        print("String is: ", self.x)

testobject = test(3)

print ("Attributes")
print (testobject.number)
print (testobject.doublenum)
print (testobject.x)

print("Calling show() function")
testobject.show()


The new part here is at the top, with the keyword class - it tells Python that we are defining a new class, followed by the name (here the class name is test). 
There are two methods (class-defined functions) for this particular class (other classes can have many more), __init__ and show
__init__ is actually a special built-in function that is useful in defining how to create new objects. It always takes at least one argument, self, which refers to the object being created, and in this case one other argument, here an integer called num. Confusingly, you don't need to include self when calling functions in classes (which should be called methods) - it is automatically included in the function call to create the class, but you need to include it in the definition. In __init__ the various attributes are initialised (given values, like variables outside objects). For each attribute initialised, you need to have self. in front. This tells __init__ that this attribute applies to this object being created. 
show is just a user-defined method that I wrote to display the attributes of the test class. Again, you need to include self as an argument in the definition, even though it is not needed in the method call. Similar to __init__ you also need to have self. in front of attribute names to tell the program which object these attributes belong to. 

testobject = test(3) is the first line outside the class definition (i.e. part of the main program) and it tells the program to create an object using the test class (sometimes called an instance of that class), with an argument of 3 and then assign that object to the variable testobjectThis is similar to a function call (with arguments included inside round brackets) but we have defined test as a class, not a function. 
The next set of print statements directly access the attributes of the testobject object, a bit like accessing a variable, but you need to state which object the attributes belong to. 

Finally testobject.show() is a method call, i.e. it tells Python to run the show() method in the testobject object. In some ways it is similar to a function call, but the method is contained within the object.
 And the result?

>>> 
 RESTART: C:/Users/John/Dropbox/Misc Programming/Python/python3/objecttest.py 
Attributes
3
6
Hello World
Calling show() function
First number is:  3
Second number is:  6
String is:  Hello World

>>>