Showing posts with label Programming. Show all posts
Showing posts with label Programming. Show all posts

Thursday, 27 December 2018

Database Programs - the Generic and the Specific

So I have been working on a Fighting Fantasy project. Before I or anyone can play the game I need to input the data for both paragraphs and monsters. Currently the SQLite database (fight.db) has 4 tables, each with a number of columns.
monster table
  • name (text, primary key)
  • skill (integer)
  • stamina (integer)
  • treasure (integer)

equip table
  • monname (text, primary key)
  • equipname (text, primary key)
  • equiptype (text)
  • lootable (integer)
  • adjust (integer)

paragraph table
  • module (text, primary key)
  • pararef (text, primary key)
  • paratext (text)
  • monster (text)
  • looseitem (text)

options table
  • module (text, primary key)
  • startpararef (text, primary key)
  • endpararef (text, primary key)
  • optiontext (text)
  • conditions (text)

I won't go into a detailed explanation of each column, suffice to say there are a mix of text and integers, and some fields are primary keys.
For those who are not familiar with primary keys, these are columns that are used to single out and identify each record. If a table has a single column as its primary key, each record in the table must have a unique value for that column. So for example, in the monster table there is one column as the primary key, name. Each entry must have a unique name value. If a table has 2 or more columns as primary keys, then the combination of those columns must be unique. For example, an entry in the paragraph table must have a unique combination of module column and pararef column.

To add new records, delete unwanted records and edit existing records in all four tables I have created two different programs using two different approaches. The first one took quite an abstract approach, treating the tables as unrelated and not really taking into account the individual needs of each table or each column. The first program uses the PRAGMA command in SQLite to retrieve information about each table, namely which columns are primary keys and also which are text or numbers. This means that the program won't try to input text values into columns for integers. It should also catch duplicate primary keys. The good news for this approach is that it is very adaptable, and if I change the tables or even use it on a different database it should still work. The program itself is 263 lines of code & comments

The second one is much more customised and takes into account the relationships between tables.
Although in Python a variable can represent a list, tuple or dictionary (a series of different values, called an iterable), in SQLite they don't use lists or other iterable datatypes - you create more tables and elements that would be in the list become records in the new table.
So if a monster has several items of equipment associated with it, rather than having a column called equip, there is a separate table. One column (here monname) says what monster record each item in the table is associated with. Here the value in monname should match up with a value in the name column of the monster table. Another column (here equipname) identifies each item associated with that monster. It is no coincidence that monname and equipname are the two columns that form the primary key for this table - the combination of monname and equipname should be unique and identify each entry in the equip table.
This is a similar situation to paragraphs and options - each paragraph typically has several options associated with it, though each option has its own characteristics associated with it.

So when entering a new paragraph, the program will ask whether the user also wants to enter options associated with that paragraph. Similarly, when deleting an unwanted paragraph, the program will offer to delete options associated with that paragraph (where startpararef value in options matches pararef value in paragraph).  Also if the primary key (pararef) value of a paragraph record is changed, the program offers to update the startpararef value of options associated with the paragraph.  These functions are also adapted to the situation with monsters and associated equipment.

This customised version is a lot longer at 665 lines - as well as writing functions that can cope with any table, I have also written functions to deal with specific tables. Is it better? When I am using it to add data to the database, yes I believe so. But I have to hope that I won't be changing the SQLite table structure because the second version assumes the database is set up in a particular way.
Incidentally, both versions can present the contents of each table in HTML. The first one can do so in a straightforward, no-frills way.

Current Contents of paragraph

modulepararefparatextmonsterlooseitem
haunted001The storm is raging, and you are pelted by rain and hail. You take shelter in the porch of an old creaking house. Perhaps you can find food and a bed for the night? You knock on the door, but it is not locked and swings open. You consider going in or looking around the outside of the house.
haunted001aYou find yourself at the front of the house. The front door is still ajar. There are stretches of garden either side to the north and south.
haunted002You walk around to the north side of the house. The windows on the ground floor are all boarded up and you cannot see in. Through the pelting rain you can see what may be a vegetable garden further to the north. The garden continues along the side of the house.
haunted003Walking around the south side of the house you come across a large pond. In the middle is a statue of what may be a satyr or a goatish demon. The water in the pond looks murky with pondweed. The south side of the house continues ahead.
haunted003aAs you step into the cold murky water the mud covers your ankles. You move in, towards the mysterious statue. You suddenly see from the corner of your eye something moving in the water towards you. Something dark green, slimy and with long limbs lurches at you from under the water. You must defend yourself against the giant frog!Giant frog
haunted005You approach the vegetable patch, and notice some figure shuffling around behind the canes supporting runner beans. Covered in rags and wielding a pitchfork it lurches towards you. As you see its rotting face, you realize the gardener is an undead zombie and you must fight it!Zombie
haunted005aYou approach the vegetable patch. The remains of a defeated zombie are scattered over the cabbages and broccoli. Just to the west you can see a surprisingly large garden shed.

The second one has the option of including the options associated with each paragraph:

Current Contents of Paragraph & Options table

modulepararefparatextmonsterlooseitemoptions
haunted 001 The storm is raging, and you are pelted by rain and hail. You take shelter in the porch of an old creaking house. Perhaps you can find food and a bed for the night? You knock on the door, but it is not locked and swings open. You consider going in or looking around the outside of the house. ('Do you go left around the north side of the house?', '', '002')
('Do you go right around the south outside of the house?', '', '003')
('Do you step into the dark house?', '', '004')
haunted 001a You find yourself at the front of the house. The front door is still ajar. There are stretches of garden either side to the north and south. ('Head left to the north side of the house?', '', '002')
('Head right to the south side of the house?', '', '003')
('Push open the door and go into the house?', '', '004')
haunted 002 You walk around to the north side of the house. The windows on the ground floor are all boarded up and you cannot see in. Through the pelting rain you can see what may be a vegetable garden further to the north. The garden continues along the side of the house. ('Go over to the gardening figure in the vegetable patch?', 'Alive:Zombie', '005')
('Carry on along the north side of the house?', '', '006')
('Head back east to the front door?', '', '001a')
('Head over to the vegetable patch to the north?', 'Dead:Zombie', '005a')
haunted 003 Walking around the south side of the house you come across a large pond. In the middle is a statue of what may be a satyr or a goatish demon. The water in the pond looks murky with pondweed. The south side of the house continues ahead. ('Wade into the pond to investigate the statue?', 'Alive:Killer frog', '003a')
('Wade into the pond to investigate the statue?', 'Dead:Killer frog', '003b')
('Head back east round to the front door?', '', '001a')
haunted 003a As you step into the cold murky water the mud covers your ankles. You move in, towards the mysterious statue. You suddenly see from the corner of your eye something moving in the water towards you. Something dark green, slimy and with long limbs lurches at you from under the water. You must defend yourself against the giant frog! Giant frog
haunted 005 You approach the vegetable patch, and notice some figure shuffling around behind the canes supporting runner beans. Covered in rags and wielding a pitchfork it lurches towards you. As you see its rotting face, you realize the gardener is an undead zombie and you must fight it! Zombie
haunted 005a You approach the vegetable patch. The remains of a defeated zombie are scattered over the cabbages and broccoli. Just to the west you can see a surprisingly large garden shed.
This gives a better idea of how the records in different tables relate to each other.

Sunday, 9 December 2018

A Script to Look at Database Files

This program arose to make things easy for me in the long run, a kind of constructive laziness (as discussed here). There are times when I am writing programs that use database files, and I forget what the table names are, or what the column names and types are, and SQLite needs these to be correctly spelt (and is case sensitive). I could open up sqlite3 on the Windows command line and try to remember how to view the metadata about the database, but that can be a bit of hassle, particularly when my mind is focused on Python code.
This program looks at that metadata and presents it in a relatively easy way.

import sys
import os
import webbrowser
import sqlite3

# Collecting list of database files
dirlist = os.listdir('C:\\sqlite\\db')
dbfilelist = []
filecount = 0
for file in dirlist:
    if file[-3:] == '.db':
        print(filecount, file, 'found')
        dbfilelist.append(file)
        filecount +=1
print(filecount, 'Completely different directory')
# Selecting which db file
filechoice = input("Enter number of file to view: ")
try:
    fileno = int(filechoice)
    chosenfile = dbfilelist[fileno]
    print(chosenfile, 'selected')
    dirpath = 'C:\\sqlite\\db\\'+chosenfile
except:
    try:
        if int(filechoice) == filecount:
            dirpath = input("Please enter new directory path including filename: ")
            # Check that new dirpath is valid file
            fcheck = open(dirpath, 'r')
            fcheck.close()
            chosenfile = dirpath
    except:
        print("Sorry, cannot parse that.")
        sys.exit()

# Starting up SQLite and selecting tables
conn  = sqlite3.connect(dirpath)
curs = conn.cursor()
curs.execute('SELECT name FROM sqlite_master WHERE type="table";')
tabdump = curs.fetchall()
tabcount = 0
print("Current tables in", chosenfile)
if len(tabdump) == 0:
    print("Database seems to be empty - quitting")
    sys.exit()
for table in tabdump:
    print(tabcount, table[0])
    tabcount += 1
tabchoice = input("Select which table to inspect or Q to quit: ").lower()
if tabchoice == 'q':
    print("Quitting - bye!")
    sys.exit()
else:
    tabname = tabdump[int(tabchoice)][0]
    print(tabname, "table info")
    curs.execute("PRAGMA table_info("+tabname+");")
    pragdump = curs.fetchall()
    collist = []
    for line in pragdump:
        print(line)
        collist.append(line[1])
    curs.execute("SELECT * FROM "+tabname+";")
    contentsdump = curs.fetchall()
    rows = len(contentsdump)
    print(tabname, "contains ", rows, "records/rows")
# Creating HTML table   
if rows > 0 and input("View table as HTML (Y)? ").lower() == 'y':
    FH = open('temp.html', 'w')
    FH.write(" \n")
    FH.write("<HTML> <BODY>\n")
    FH.write("<TABLE Border=1> <TR>")
    for col in collist:
        FH.write("<TD><B>"+col+"</B></TD>")
    for line in contentsdump:
        FH.write("<TR> \n")
        for cell in line:
            FH.write("<TD>"+str(cell)+"</TD>")
        FH.write("</TR> \n")
    FH.write("</TABLE> \n")
    FH.write("</BODY> </HTML> \n")
    FH.close()
    webbrowser.open('temp.html')
   
conn.close()



This script works well and makes relatively few assumptions. As a matter of good habit, I create and keep database files in 'C:\SQLite\db' folder in Windows, and I always give them a '.db' extension at the end of the filename. This script assumes the user wants that folder, but the user can go for a completely different directory path. This option also allows for users to try to open files without the .db extension.  The os module proved useful in listing files.
The sys module was used to exit gracefully in a number of situations where the program could not progress.
The tables within each database can be found by looking at the sqlite_master table, which holds other information as well.
The SQLite PRAGMA command was useful in getting information about a table in the database, including the columns in the table.
Finally there is the option of viewing an HTML version of the table, using a similar method to that used before in my previous programs.
Typical results are shown here:

= RESTART: C:/Users/666/Dropbox/Misc Programming/Python/python3/db_viewer.py =
0 base1.db found
1 biology.db found
2 dragon.db found
3 example.db found
4 scimitar.db found
5 Completely different directory
Enter number of file to view: 0
base1.db selected
Current tables in base1.db
0 MonstrousTroops
1 loot
Select which table to inspect or Q to quit: 0
MonstrousTroops table info
(0, 'Name', 'TEXT', 0, None, 0)
(1, 'Challenge', 'REAL', 0, None, 0)
(2, 'Army', 'TEXT', 0, None, 0)
(3, 'Notes', 'TEXT', 0, None, 0)
MonstrousTroops contains  29 records/rows
View table as HTML (Y)? n


Tuesday, 4 December 2018

Historical Database ver5 - combining SQLite, Python and HTML

I have had a go at updating the program for looking at and managing the database of historical figures. Last post I was busy altering the SQLite database to add a new column (imagelink, for images associated with each record) and remove an unwanted column (Role, which has been replaced by a subtable to allow multiple roles per personality).

Now I have been modifying the Python program firstly to accommodate the changes in the data structure, and also to display any selected record as a HTML page. This is for two reasons:
Firstly HTML pages can display images when opened in a browser; Secondly HTML can take web links in the database and turn them into clickable hyperlinks. You can't do either of these just on the IDLE command line.(A third reason that isn't put into practice here is HTML makes tables easier, particularly automating column width - useful when you want to display data in tabular form).

 The whole program is now 415 lines long, so I won't just copy and paste the whole thing. That's one of the things I've noticed since starting out learning Python. When you keep working on a program, it can grow surprisingly big. The trick is to make sure that big does not mean unreadable or unmanageable (functions really help here, as do comments in the code).
Making changes to the data structure can mean lots of changes to Python code, and I've been going through the code and testing it, seeing what the ramifications of changing the columns are for the code. This has affected editing existing records, entering new records, displaying the main table and of course displaying records.

This is the function that has changed the most, the one for displaying individual records.

def displayrecord(name):
    curs.execute("SELECT * FROM historicalfigures WHERE Name = '"+ name +"';")
    maindump = curs.fetchall()
    curs.execute("SELECT * FROM roles WHERE Name = '"+ name +"';")
    rolesdump = curs.fetchall()
    curs.execute("SELECT * FROM weblinks WHERE Name = '"+ name +"';")
    webdump = curs.fetchall()
    mainrecord = maindump[0]
    FileH = open("temp.html", 'w')
    FileH.write("<HTML>")
    FileH.write("<h3>Name: " + name +"</h3> \n")
    FileH.write("<p>Born: " + str(mainrecord[1])+"</p> \n")
    FileH.write("<p>Died: " + str(mainrecord[2])+"</p> \n")
    FileH.write("<p>Nation: " + mainrecord[3]+"</p> \n")
    FileH.write("<p>Image: " + str(mainrecord[4])+"</p> \n")
    FileH.write("<br><img src='"+str(mainrecord[4])+"' width=300 align='right'>")
    rolestring = "<p><B>Roles</B></p> <ul>\n"
    for line in rolesdump:
        rolestring = rolestring + "<li>"+ line[1] +"</li> \n"
    rolestring += "</ul> \n"
    FileH.write(rolestring)
    if webdump: # does webdump contain any lines?
        FileH.write("<p><b>Web addresses for more info</b></p> \n")
        FileH.write("<ul>")
        for line in webdump:
            FileH.write("<li><a href ='"+line[1]+"'>"+line[2] +"</a></li> \n")
        FileH.write("</ul>")
    FileH.write("</HTML>")
    FileH.close()
    webbrowser.open("temp.html")



As you can see it first collects the data from SQLite (using the curs cursor object as usual), then creates a file (temp.html) and file handle (a.k.a. io stream) so Python can write to it, and then writes HTML code to the file modified by what the data in the database is. Finally it uses the webbrowser module (imported along with sqlite3 module earlier in the code) to open the newly created web page in your web browser.

Here is the HTML contents of a typical record:

<HTML><h3>Name: Cardinal Thomas Wolsey</h3>
<p>Born: 1473</p>
<p>Died: 1530</p>
<p>Nation: England</p>
<p>Image: https://collectionimages.npg.org.uk/large/mw06903/Thomas-Wolsey.jpg</p>
<br><img src='https://collectionimages.npg.org.uk/large/mw06903/Thomas-Wolsey.jpg' width=300 align='right'><p><B>Roles</B></p> <ul>
<li>Politician</li>
<li>Cleric</li>
</ul>
</HTML>

Any web designer will tell you this is not exactly attractive, but I believe it serves as proof of concept - we can use Python to get data from a database and create a custom webpage using that data.

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.   



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

>>> 



Wednesday, 31 October 2018

Creating web pages using Python

Much of today's computer usage that I am aware of occurs on the World Wide Web. Social media, news websites, forums, wiki pages, homepages for movie streaming, internet shopping and auctioning, the list goes on.
Can you create web pages with Python? Yes.
Let me qualify that.  Python can help automate and speed up writing the HTML. However, on its own it is not a web hosting service, so using it to create web pages that the rest of the world can visit will require more than just Python.

In terms of viewing web pages and opening up web browsers, the module I have found useful is webbrowser, which can be imported like other modules. For example, creating a very simple web page in Python then opening it in a browser:

import webbrowser
fhandle = open('temp.html', 'w')
fhandle.write("<!DOCTYPE html> \n")
fhandle.write("<html> \n")
fhandle.write("<body> \n")
fhandle.write("<H1>Hello World Wide Web!</H1> \n")
fhandle.write("</body> \n")
fhandle.write("</html> \n")
fhandle.close()

webbrowser.open_new_tab('temp.html')

HTML (Hyper Text Mark-up Language) is of course the basic language of the web, and many pages are created using it (especially the older ones). Like Python it is a computer language, but while Python is a programming language and SQLite is a query language, HTML is a mark-up language, which basically tells the web browser on your computer how to display the content downloaded as the web page. I won't go into the details of writing HTML, but there are plenty of books and web pages to help you. Here we are creating the HTML as strings and writing it into the new file (here temp.html).

You can also use the webbrowser module to open pages hosted elsewhere if you know the web address. For example:

import webbrowser
webpage = "https://www.bbc.co.uk/news"
webbrowser.open(webpage)

webpagelist = ["https://apod.nasa.gov/apod/astropix.html",
               "https://en.wikipedia.org/wiki/Main_Page",
               "https://www.youtube.com/?hl=en-GB&gl=GB"]
for page in webpagelist:
    webbrowser.open(page)


The webbrowser module has several functions, including
webbrowser.open('web address') 
This is the basic function that opens the web page in the default browser.

webbrowser.open_new('web address')
This function opens the page in a new window in the default browser.

webbrowser.open_new_tab('web address')
And this one opens the page in a new tab.
You don't really need Python to open up a web page, and it's a programming language, not a search engine. So why bother?
The real benefit of Python is in creating new web pages where the content is contingent on inputs, i.e. web pages that have variable content. I have already used Python to create HTML tables. This module allows you to create and display the table in the same program. So if we have a text file with the following content:
Aardonyx, Africa, 195, Sauropod-prosauropod, https://en.wikipedia.org/wiki/Aardonyx
Abelisaurus, S-America, 80, Therapod-abelisaur, https://en.wikipedia.org/wiki/Abelisaurus
Abrictosaurus, Africa, 200, Ornithopod, https://en.wikipedia.org/wiki/Abrictosaurus

Abydosaurus, N-America, 105, Sauropod, https://en.wikipedia.org/wiki/Abydosaurus 
..
.. 
.. 

we can create an HTML table using the contents and even turn the web addresses for each line into hyperlinks. For those who are wondering, this text file is from a previous project from before I started this blog, with 150 dinosaur genera.
Here is the program that does that.
import webbrowser
fhandle = open('dinodata2.txt', 'r')
totallist = []
for line in fhandle:
    linelist = line.split(',')
    linelist[4].rstrip()
    totallist.append(linelist)

fhandle.close()
xhandle = open('tempweb.html', 'w')
xhandle.write("""
<!DOCTYPE html>
<html>
<head>
<title>Dinosaur List</title>
</head>
<body>
<h1>Dinosaur List</h1>
<table style="border: 1px solid black;">
<tr><td>Genus Name</td><td>Continent</td><td>Millions of Years Ago</td><td>Group</td><td>Wiki Link</td></tr>
""")

for line in totallist:
    xhandle.write("<tr>")
    for cell in line:
        if cell == line[4]:
            xhandle.write("<td style='border: 1px solid black;'><a href='"+cell+"'>Wikipedia Link</a></td>")
        else:
            xhandle.write("<td style='border: 1px solid black;'>" +cell+"</td>")
    xhandle.write("</tr>")
xhandle.write("""
</table>
</body>
</html>""")

xhandle.close()
webbrowser.open('tempweb.html')


The results are basic - someone who knows either HTML or CSS (Cascading Style Sheet) formatting could add more style, but the important thing is it reads from a text file and creates a web page with a table, filled with data from the text file, and hyperlinks for web addresses (here the relevant Wikipedia page). It actually stores the content from the text file as a list of lists (here totallist), then uses that list of lists to create the html table. I guess the next step would to be add sorting and filters to this table so you can look for dinosaurs according to parameters.
The final line webbrowser.open('tempweb.html') shows that the webpage can be local rather than on a remote server - the web address can be replaced by a file path on the local drive or network. Here the file tempweb.html is created in the same folder as the Python program. 

Tuesday, 23 October 2018

Failing Gracefully

Failing Gracefully is the idea that if something goes wrong when a program is running, the disruption this causes to the user is minimised.  If possible the program keeps going, and even if it has to close, at least it can be polite and informative to the user as it does so. Ideally an end user should never have to see a raw syntax error on their screen. At the end of my previous post when discussing the historical figure database I said
 If the inputs from the user are valid it does not throw any errors. I have not quite ironed out all the errors caused by bad inputs.
This is one important aspect of failing gracefully - coping with user error. Clearly the program isn't good if it fails and throws a syntax error when the user enters valid inputs. But a good program should cope with invalid inputs as well. In terms of the latest program I have created, most of the inputs are tested to see that they valid - there is a function in the program that is dedicated to making sure that inputs that are expected to be integers are indeed integers:

def checkint(num):
    try:
        int(num)
        check = True
    except:
        print("Not a valid number!")
        check = False
    return check

Simple but it does its job. Note that it uses a try...except... structure, which is a very useful thing when checking for something that might cause syntax errors. If a syntax error would occur in a try...except... structure, it is caught before the program crashes, and the except part takes over. 
Another aspect and set of errors is when the program needs to connect to an external resource, such as a file. This can happen when either opening a text file or when using the SQLite module to connect to a database file, or maybe trying to connect to a website. If the file doesn't exist, or maybe not in the folder the program is using as the path, then generally the program will throw an error and end. Are there alternatives? Could the programmer anticipate this? If the external file is essential for the running of the program then the best case scenario may be that the Python program closes, leaving an error message saying that it could not find the relevant file. Should the user be prompted to enter an alternative file path? Could the program run with a different file? Should this alternative file's name be included in the code or should the user be prompted to input it? As above, a try...except... structure can catch these problems before they cause the program to crash. 
Asking the programmer to anticipate all of these problems could add substantially to the code, but not anticipating them at all is courting even more problems, especially when untrained users are let loose on the program. I admit that I do make assumptions about things working as expected (including user inputs) when creating the programs you have already seen on this blog. For example, the historical figures database assumes that SQLite is already installed, and that the history1.db with its three tables and their respective columns is already set up, ready to at least receive new data. That's simply how things are on my computer. Running the program on a different computer will cause problems because the program does not anticipate these differences between computers.

Monday, 15 October 2018

The Historic Figures Database Program

The full code for version 4 of the program is as follows:

import sqlite3
conn = sqlite3.connect("C:\\sqlite\\db\\history1.db")
curs = conn.cursor()

#List of User-defined Functions
#addlink()
#addrole()
#checkint()
#deleterecord()
#deleterole()
#deletelink()
#displayrecord()
#displaytable()
#enternew()
#searchtable()
#updaterecord()
#viewtable()

def addlink():
    print("Adding Web link")
    curs.execute("SELECT Name FROM historicalfigures ORDER BY Name;")
    namedump = curs.fetchall()
    namelist = []
    for line in namedump:
        namelist.append(line[0])
        print (namelist.index(line[0]), line[0])
    namech = input("Enter number of name to add web link to: ")
    if checkint(namech) and int(namech) < len(namelist):
        newname = namelist[int(namech)]
        print("Record selected is " + newname)
        linkaddress = input("Please enter web address: ")
        linknote = input("Note to describe link? ")
        keeper = input("Do you wish to keep this new link (Y/N)? ")
        keeper = keeper.lower()
        if keeper == 'y':
            curs.execute("INSERT INTO weblinks (Name, weblink, notes) VALUES ('"+newname+"', '"+linkaddress+"', '"+linknote+"');")
            curs.execute("COMMIT;")

def addrole():
    curs.execute("SELECT Name from historicalfigures ORDER BY Name;")
    namedump = curs.fetchall()
    namelist = []
    for line in namedump:
        namelist.append(line[0])
        print (namelist.index(line[0]), line[0])
    namech = input("Enter number of name to add role to: ")
    if checkint(namech) and int(namech) < len(namelist):
        newname = namelist[int(namech)]
        Role = input("Please enter new role: ")
        keeper = input("Do you wish to keep this new role (Y/N)? ")
        keeper = keeper.lower()
        if keeper == 'y':
            curs.execute("INSERT INTO roles (Name, Role) VALUES ('"+newname+"', '"+ Role +"');")
            curs.execute("COMMIT;")

def checkint(num):
    try:
        int(num)
        check = True
    except:
        print("Not a valid number!")
        check = False
    return check
   

def deleterecord():
    curs.execute("SELECT Name FROM historicalfigures ORDER BY Name;")
    namedump = curs.fetchall()
    print("Current list of names is:")
    linecount = 0
    for line in namedump:
        print(str(linecount) + " - " + line[0])
        linecount += 1
    whichline = input("Enter number of row to be deleted: ")
    whichline = int(whichline)
    delname = namedump[whichline][0]
    confirm = input("Please confirm (Y) or cancel (N) deleting record for " + delname +": ")
    confirm = confirm.lower()
    if confirm == 'y':
        curs.execute("DELETE FROM historicalfigures WHERE Name ='" + delname + "';")
        curs.execute("DELETE FROM roles WHERE Name = '"+ delname + "';")
        curs.execute("DELETE FROM weblinks WHERE Name = '"+ delname + "';")
        curs.execute("COMMIT;")

def deletelink():
    curs.execute("SELECT Name, notes, weblink FROM weblinks ORDER BY Name;")
    linkdump = curs.fetchall()
    rowcount = 0
    for line in linkdump:
        print(rowcount, line[0], line[1], line[2])
        rowcount += 1
    linechoice = input("Which row should be deleted?")
    if checkint(linechoice) and int(linechoice) < rowcount:
        rowchoice = int(linechoice)
        print("You have selected: ")
        print(linkdump[rowchoice][0], linkdump[rowchoice][1], linkdump[rowchoice][2])
        confirm = input("Confirm delete (Y/N)?")
        confirm = confirm.lower()
        if confirm == 'y':
            curs.execute("DELETE FROM weblinks WHERE Name = '"+ linkdump[rowchoice][0] +"' AND weblink = '"+ linkdump[rowchoice][0] +"';")
   
def deleterole():
    curs.execute("SELECT Name, Role FROM roles ORDER BY Name;")
    roledump = curs.fetchall()
    nameset = set()
    for line in roledump:
        nameset.add(line[0])
    namelist = list(nameset)
    namelist.sort()
    print("Deleting from Roles Table")
    for name in namelist:
        print (namelist.index(name), name)
    numchoice = input("Please enter which name to delete role from: ")
    namechoice = namelist[int(numchoice)]
    rolelist = []
    for line in roledump:
        if line[0] == namechoice:
            rolelist.append(line[1])
    for r in rolelist:
        print (rolelist.index(r), r)
    delrolenum = input("Which role number do you wish to delete? ")
    if checkint(delrolenum) and int(delrolenum) < len(rolelist):
        delrole = rolelist[int(delrolenum)]
        confirm = input("Confirm delete " + namechoice + " role as " + delrole + "(Y/N)? ")
        confirm = confirm.lower()
        if confirm == 'y':
            curs.execute("DELETE FROM roles WHERE Name = '"+ namechoice +"' AND Role = '"+ delrole +"';")
            curs.execute("COMMIT;")
            curs.execute("SELECT Role FROM roles WHERE Name = '"+ namechoice +"';")
            currentrolesdump = curs.fetchall()
            currentroles = ''
            for line in currentrolesdump:
                for col in line:
                    currentroles += col + ', '
            print("Current roles are now: " + currentroles)
       
   
def displayrecord(name):
    curs.execute("SELECT * FROM historicalfigures WHERE Name = '"+ name +"';")
    maindump = curs.fetchall()
    curs.execute("SELECT * FROM roles WHERE Name = '"+ name +"';")
    rolesdump = curs.fetchall()
    curs.execute("SELECT * FROM weblinks WHERE Name = '"+ name +"';")
    webdump = curs.fetchall()
    mainrecord = maindump[0]
    print("Name: " + name)
    print("Born: " + str(mainrecord[1]))
    print("Died: " + str(mainrecord[2]))
    print("Nation: " + mainrecord[4])
    rolestring = ""
    for line in rolesdump:
        rolestring = rolestring + line[1] +", "
    print("Roles: " + rolestring)
    if webdump: # does webdump contain any lines?
        print("Web addresses for more info")
        for line in webdump:
            print(line[1])

def displaytable(dump):
    print("No.  | Name                  | Born  | Died  | Nation")
    print("=====|=======================|=======|=======|=====================")
    counter = 1
    for line in dump:
        counterstr = " "* (4- len(str(counter)))+ str(counter) +" | "
        namestr = line[0]
        if len(namestr) >= 23:
            namestr = namestr[0:22]
        nationstr = line[4]
        if len(nationstr) >= 20:
            nationstr = nationstr[0:19]
        gapname = " " * (22 - len(namestr)) + "| "
        gapborn = "  | "
        gapdied = "  | "
        print(counterstr + namestr + gapname + str(line[1]) + gapborn + str(line[2]) + gapdied + nationstr)
        counter += 1
    print("Enter the record number if you want further details or just enter to return to main menu.")
    displaychoice = input("? ")
   
    if checkint(displaychoice):
        choi = int(displaychoice) -1
        if choi >= 0 and choi < (len(dump) +1):
            displayrecord(dump[choi][0])

def enternew():
    print("Entering new record")
    newname = input("Please enter new name: ")
    newbirth = input("Please enter new year of birth: ")
    if not checkint(newbirth):
        print("Not an integer.")
        return
    newdeath = input("Please enter new year of death: ")
    if not checkint(newdeath):
        print("Not an integer.")
        return
    newnation = input("Please enter new nation: ")
    newrole = input("Please enter new roles, separated by commas: ")
    templist = newrole.split(',')
    rolelist = []
    for r in templist:
        if r[0] == ' ':
            r = r[1:]
        r = r.rstrip()
        rolelist.append(r)
    print(rolelist)
    keeper = input("Do you want to keep this new record (Y to keep, N to discard): ")
    keeper = keeper.lower()
    if keeper == "y":
        curs.execute("INSERT INTO historicalfigures (Name, YearBirth, YearDeath, Nation) VALUES ('" + newname + "', "+ newbirth +", "+ newdeath +", '" +newnation +"');")
        for newrole in rolelist:
            print("INSERT INTO roles (Name, Role) VALUES ('" + newname + "', '" + newrole +"');")
            curs.execute("INSERT INTO roles (Name, Role) VALUES ('" + newname + "', '" + newrole +"');")
        curs.execute("COMMIT;")
   
def searchtable():
    print("Searching table - please select which column ")
    print("Name ----(N)")
    print("Born ----(B)")
    print("Died ----(D)")
    print("Nation --(T)")
    print("Role ----(R)")
    fieldchoice = input("? ")
    fieldchoice = fieldchoice.lower()
    if fieldchoice == "n":
        term = input("What name? ")
        searchterm = "Name = '" + term + "'"
    elif fieldchoice == "b":
        compare = input("Do you want results before ( < ), in exact year ( = ) or after ( > )? ")
        term = input("Which Year? ")
        if checkint(term):
            searchterm = "YearBirth "+ compare + " " + term
        else:
            return
    elif fieldchoice == "d":
        field = "YearDeath"
        compare = input("Do you want results before ( < ), in exact year ( = ) or after ( > )? ")
        term = input("Which Year? ")   
        if checkint(term):
            searchterm = "YearDeath "+ compare + " " + term
        else:
            return
    elif fieldchoice == "r":
        term = input("What Role? ")
        term = term.capitalize()
        searchterm = " Name in (SELECT Name FROM roles WHERE Role = '"+ term +"')" # To be completed
    elif fieldchoice == "t":
        term = input("What Nation? ")
        searchterm = "Nation LIKE '%" + term + "%'"
    else:
        print("Sorry, option not recognised. ")
        return()
    executable = "SELECT * FROM historicalfigures WHERE " + searchterm +";"
    print (executable)
    curs.execute(executable)
    searchdump = curs.fetchall()
    if searchdump:
        displaytable(searchdump)
    else:
        print("Sorry, no records found that match the criteria")

def updaterecord():
    curs.execute("SELECT Name FROM historicalfigures ORDER BY Name;")
    namedump = curs.fetchall()
    print("Current list of names is:")
    linecount = 0
    for line in namedump:
        print(str(linecount) + " - " + line[0])
        linecount += 1
    whichline = input("Enter number of row to be changed: ")
    if checkint(whichline):
        whichline = int(whichline)
    else:
        print("Sorry, not a valid selection")
        return
    updatename = namedump[whichline][0]
    curs.execute("SELECT * FROM historicalfigures WHERE Name = '"+ updatename+"';") 
    changedump = curs.fetchall()
    curs.execute("SELECT Role FROM roles WHERE Name ='" + updatename + "';")
    roledump = curs.fetchall()
    coltuple = ('Name', 'YearBirth', 'YearDeath', 'Role', 'Nation')
    colcount = 0
    for col in changedump[0]:
        print (str(colcount), coltuple[colcount], changedump[0][colcount])
        colcount += 1
    for row in roledump:
        print ("role " + str(colcount), row[0])
        colcount += 1
    colchoice = input("Please enter number of column to change: ")
    colchoice = int(colchoice)
    newvalue = input("Please enter new value for this column: ")
    if colchoice == 0 or colchoice == 3 or colchoice == 4:
        newvalue = "'"+ newvalue +"'"
        table = "historicalfigures"
        field = coltuple[colchoice]
        if colchoice == 0:
            print("Updating Name will affect records in other tables that use this name")
    elif colchoice == 1 or colchoice == 2:
        field = coltuple[colchoice]
        table = "historicalfigures"
        if not checkint(newvalue):
            return
    elif colchoice > 4 and colchoice < colcount:
        table = "roles"
        field = "role"
        newvalue = "'"+ newvalue +"'"
        oldvalue = roledump[colchoice -5][0]
       
    print("About to change "+ field +" to " + newvalue + " for "+ updatename)
    confirmchange = input("Confirm(Y) or discard (N): ")
    confirmchange = confirmchange.lower()
    if confirmchange == 'y':
        if table == "historicalfigures":
            executable = "UPDATE "+ table +" SET "+ field +" = "+ newvalue +" WHERE Name = '" + updatename + "';"
        elif table == "roles":
            executable = "UPDATE "+ table +" SET "+ field +" = "+ newvalue +" WHERE Name = '" + updatename + "' AND role = '" + oldvalue + "';"
        #print(executable)
        curs.execute(executable)
        if colchoice == 0:
            executable_role = "UPDATE roles SET Name = "+ newvalue +" WHERE Name = '" + updatename + "';"
            print(executable_role)
            curs.execute(executable_role)
            executable_link = "UPDATE weblinks SET Name = "+ newvalue +" WHERE Name = '"+ updatename + "';"
            print(executable_link)
            curs.execute(executable_link)
        curs.execute("COMMIT;")
   
def viewtable():
    print("Viewing Main Table")
    print("Sort table by: N for name, B for year of birth, D for year of death, T for nation")
    sortby = input("? ")
    sortby = sortby.lower()
    sortdic = {'n':'Name',
               'b':'YearBirth',
               'd':'YearDeath',
               't':'Nation'}
    if sortby in sortdic.keys():
        sortfield = sortdic[sortby]
    else:
        print('Sorry, option not recognised')
        return
   
    curs.execute("SELECT * FROM historicalfigures ORDER BY "+ sortfield +";")
    fulldump = curs.fetchall()
    displaytable(fulldump)
   


#Main program
print ("Historical Figures Table")
print ("========================")
cont = True
while cont == True:
    print ("Main menu - Please select an option")
    print ("View whole table ---V")
    print ("Search table -------S")
    print ("Enter new record ---E")
    print ("Delete record ------D")
    print ("Update record ------U")
    print ("Quit Program -------Q")
    print ("Add/Delete Role ----R")
    print ("Add/Delete Weblink -W")
    optchoice = input("? ")
    optchoice = optchoice.lower()
    if optchoice == 'v':
        viewtable()
    elif optchoice == 's':
        searchtable()
    elif optchoice == 'e':
        enternew()
    elif optchoice == 'd':
        deleterecord()
    elif optchoice == 'u':
        updaterecord()
    elif optchoice == 'q':
        conn.close()
        cont = False
    elif optchoice == 'r':
        rolech = input ("Add (A) or Delete (D)? ")
        rolech = rolech.lower()
        if rolech == 'd':
            deleterole()
        elif rolech == 'a':
            addrole()
    elif optchoice == 'w':
        linkch = input("Add (A) or Delete (D)? ")
        linkch = linkch.lower()
        if linkch == 'd':
            deletelink()
        elif linkch == 'a':
            addlink()
    else:
        print("Sorry, not recognised")



That is a lot - Sorry about that for those who don't like wading through pages of code. But you can see why my rate of posting has slowed.
So does it work? Generally speaking yes. It does what I intended it to. If the inputs from the user are valid it does not throw any errors. I have not quite ironed out all the errors caused by bad inputs. There are also some improvements that could be made, but I've decided to leave those to version 5 of the program.