Header Image - Randall Morgan

Tag Archives

6 Articles

Sweet16-GP Assembler – Part 2

by SysOps 0 Comments
This entry is part 7 of 8 in the series Sweet16-GP CPU: A Complete Development Cycle

Last time we left our assembler with only two functions. The strip_lines() function which removes comments and blank lines and the rename_registers() function which replaces the register’s friendly names with the cononical names i.e. Rx where x is a number between 0 and 7 (the register’s index value into the register file).

With these to pre-processing functions out of the way we are ready to start processing our assembly code.  We will use a two-pass method of assembly. The first pass will break each line into its constituent parts and then locate the instruction’s addressing mode, stores it in a field called mode, and calculate the instruction’s actual opcode value and length. In addition it will store a labels in a symbol table to record their value for later use in the second pass or the assembler.

Our main assembler method will take our clean lines, one at a time, and parse them as described above. We will need a table of instructions that includes details about each instruct including it’s addressing mode(s), opcode(s) and length. You may be wondering why the “(s)” above. This is because some of our instructions have multiple addressing modes and therefore multipple opcodes and perhaps even multiple instruction lengths. We will use our table to disambiguate the instruction format into the proper values. 

We will implement our opcode_table as a dict. Each entry will be  indexed (key) by the instruction mnemonic.  The value for each key in the table will be a dict indexed by the addressing mode. Each addressing mode will then have a list of values containing the instruction’s opcode. If the opcode requires a one or two by value, they will be provided as VALUE_L and VALUE_H in the list. This description sounds much more complex than it really is. Refere to the code for our table below and re-read the above paragraph. It should all become clear.

opcode_table = {
    'HALT': {
        'implicit': ['0x00'],
    },
    'BRA': {
        'immediate': ['0x01', VALUE_L],
        'offset': ['0x01', OFFSET],
    },
    'BRC': {
        'immediate': ['0x02', VALUE_L],
        'offset': ['0x02', OFFSET],
    },
    'BRZ': {
        'immediate': ['0x03', VALUE_L],
        'offset': ['0x03', OFFSET],
    },
    'BRN': {
        'immediate': ['0x04', VALUE_L],
        'offset': ['0x04', OFFSET],
    },
    'BRV': {
        'immediate': ['0x05', VALUE_L],
        'offset': ['0x05', OFFSET],
    },
    'BSR': {
        'immediate': ['0x06', VALUE_L, VALUE_H],
        'offset': ['0x06', LABEL_L, LABEL_H],
    },
    'RTS': {
        'implicit': ['0x07'],
    },

    'SET': {
        'direct': ['0x08', VALUE_L, VALUE_H],
    },
    'LD': {
        'register': ['0x10'],
        'indirect': ['0x20'],
    },
    'ST': {
        'register': ['0x18'],
        'indirect': ['0x28'],
    },
    'LDD': {
        'indirect': ['0x30'],
    },
    'STD': {
        'indirect': ['0x38'],
    },
    'POP': {
        'indirect': ['0x40'],
    },
    'STP': {
        'indirect': ['0x48'],
    },
    'ADD': {
        'register': ['0x50'],
    },
    'SUB': {
        'register': ['0x58'],
    },
    'MUL': {
        'register': ['0x60'],
    },
    'DIV': {
        'register': ['0x68'],
    },
    'AND': {
        'register': ['0x70'],
    },
    'OR': {
        'register': ['0x78'],
    },
    'XOR': {
        'register': ['0x80'],
    },
    'NOT': {
        'register': ['0x88'],
    },
    'SHL': {
        'register': ['0x90'],
    },
    'SHR': {
        'register': ['0x98'],
    },
    'ROL': {
        'register': ['0xA0'],
    },
    'ROR': {
        'register': ['0xA8'],
    },
    'POPD': {
        'indirect': ['0xE0'],
    },
    'CPR': {
        'register': ['0xE8'],
    },
    'INC': {
        'register': ['0xF0'],
    },
    'DEC': {
        'register': ['0xF8'],
    },
}

As you can see, our instructions can fall into one of four addressing modes. These include implicit (when the instruction itself indicates the addressing mode),  immediate (when the value needed by the instruction directly follows it), indirect (when the register specified contains the value needed to complete the instructions, and register (when the instruction operates directly on the register specified). To tell the truth, these could be further broken down but that would only complicate things.

OK, now we have our opcode_table we are going to start writing our parser. Our parser is a little bit unorthodox. Since our input language is so constrained we can forego all the formal theory and much of the discipline or parser development. 

I do believe however, that any developer worth their weight should write at least a simple parser at some point. Understanding even a very simple parser expands your understanding of how your tools work whether you’re a web developer, AI master, ETL programmer, or Embedded Systems developer. If you call yourself a Software Engineer or Programmer and you have written at least a simple parser. compiler, or interpreter, then you owe it to yourself to do so! To get you started I recomend DR. Jack Crenshaw’s “Let’s Build A Compiler” and Ruslans’s “Let’s Build A Simple Interpreter” blog serries. If you’re a seasoned developer checkout the book “Language Implementation Patterns”. For those mathematically oriented, checkout the Dragon Book

So what I am going to do is simply plow forward. We will tackle one issue at a time and add functions to handle each issue as it arises. Note that this technique wont work if you’re trying to write a high level language compiler or interpreter. There you will need all the formal methods and a have good design before writing code. What we are going for here is a quick and simple boot-strap assembler. We can write better tools later. For now, we just need something to get us up and running.

OK, the first thing we are going to need is some set of methods to break up the line of source code we feed to our assembler into it’s various fields. We will create an entry point function called assemble which will do our heavy lifting by calling other functions.

We will need to produce some data about the current source line. This will include any label, register, addressing mode, and value. Additionally, we may also want to keep track of the original instruction mnemonic and any intermediate code we produce. So we will need a supporting function to handle this. We will also need some storage for any lables or symbols we encounter during parsing.

The code below is our entry point. It will be responsible for handling our two apasses over our code and  will call the functions that do the heavy lifting.

def assemble(lines, lc=0):
    """Breaks the line into its constituent parts.
        it then locates the instruction's addressing mode,
        stores it in mode, and calculates the instruction's
        actual opcode value and length. """
    objcode = []
    symbols = {}

    # Pass 1 : Parse instructions and create intermediate code
    for lineno, label, (value, register, mode, mnemonic, icode) in parse_lines(lines, symbols):
        # Try to evaluate numeric labels and set the lc (location counter)
        if label:
            try:
                lc = int(eval(label, symbols))
            except (ValueError, NameError):
                symbols[label] = lc

        # Store the resulting object code for later expansion
        if icode:
            objcode.append((lineno, lc, value, register, mode, mnemonic, icode))
            lc += len(icode

Note the call to parse_lines() we will write this function next. But, let’s step back and think about what we need it to do…

Given a line of code like:

start:    SET   R1, 0xFA

We can see we need to break up this line into it’s smaller parts. Luckily, this is pretty easy to do. Also, since each line will follow a similar pattern for the fields, we can easily deduce what each field contains. 

# Exception used for errors
class AssemblyError(Exception):
    pass

def parse_lines(lines, symbols):
    """ Determine line number, mnemonic, register, value, and address mode"""
    for lineno, line in enumerate(lines, 1):
        # Handle labels
        label, *colon, statement = line.rpartition(":")
        try:
            # parse the line into ir (intermediate representation)
            data = lineno, label, parse_opcode(statement, symbols) if statement else (None, None, None, None, None)
            yield data
        except AssemblyError as e:
            print("{0:4d} : Error : {1}".format(lineno, e))

Refering to the code above, we use the enumerate function to keep track of our line numbers. One drawback to doing this approach is that our comment and blank lines wont be counted. Something we could fix by tracking line numbers in the strip_lines() function. But for now, we will do it this way.

Enumerate returns to us an integer value for the line number (lineno) and the line of source code. Next, we use rpartition(“:”) on on the source line to break it into three parts. These parts are return in a tuple containing the part before the seperator (colon in our case, which ends a label declaration), the seperator, and the part after the seperator. If any part is not found (for example, if there is no colon in the source line) then that part returns an empty string value. So if our line does not contains a colon, we will get an empty string for the label and for the seperator followed by the initial line of code which we save in the statement variable.

Once we have gotten any label, we need to take our statement and further parse it into its various parts. If it turns out that our statement is empty, we need to return a default tuple of ‘None’ values otherwise, we make a call to parse_opcode() passing in our statement and our symbol table so that any symbols found in our source line can be handled. If this all goes horribly wrong, we throw an exception and print the error message. We defined a simple exception class above for this purpose.

Our next task is to implement a function to parse the line into opcodes. We called this function pasre_opcode() above. The implementation is shown below:

def parse_opcode(line: str, symbols) -> tuple:
    """ Break the line into its constituent parts.
        Locate any labels and store their definition.
        Then locate the instruction's addressing mode,
        and stores it in mode. Calculate the instruction's
        actual opcode value.

        Returns: tuple(value, register, mode, objcode) where
        value is a string to be evaluated in the second pass.
        register is the register value or empty string if no
        register exists. "mode" is the addressing mode and
        objcode is a dict containing the base opcode value,
        and a list of functions needed to process the
        instruction
    """
    fields = line.split(None, 1)
    nofields = len(fields)
    if nofields > 1:
        extra = fields[1].split(',')
        if len(extra) > 1:
            fields[1] = extra[0]
            fields.extend(extra[1:])
    nofields = len(fields)
    mnemonic = fields[0]
    register = ''
    value = ''

    """ Examples:
            HALT
            BRA 0x1F
            SET R0, 0xFFFE
            LD R2
            STD @R3  
    """
    # Get register and value
    if nofields > 1:
        register = parse_register(fields[1])

    if register == '' and nofields > 1:
        value = parse_value(fields[1])

    if nofields > 2:
        register = parse_register(fields[1])
        value = parse_value(fields[2])

    # Get address mode
    mode = parse_address_mode(fields)

    # Get all addressing modes for this mnemonic
    opcodemodes = opcode_table.get(mnemonic)
    if not opcodemodes:
        raise AssemblyError("Unknown opcode '{}'".format(mnemonic))

    # Get the address mode used in the instruction
    objcode = opcodemodes.get(mode)
    if not objcode:
        raise AssemblyError("Invalid addressing mode '{0}' for {1}".format(mode, mnemonic))

    return value, register, mode, mnemonic, list(objcode)

There is a lot going on here so let’s unpack it. First, we use the split() method to split the statement into two portions. The first parameter to split() gives the character to split on and the second parameter is used to limit the number of splits. What we are doing here is seperating the instruction mnemonic from the rest of the statement. Since some instructions (i.e.: HALT) only have the mnemonic we need to check the number of fields we get back. If it’s only one, then we have an instruction like HALT or RTS. 

However, if we have more than one field returned from the split we need to further decompose the line. We now have a mnemonic in fields[0] and the remainder of the line in fields[1]. We now know we may have something like Rx, @Rx, or Rx, 0xFA following the instruction mnemonic. So the next move is to tray and seperate the remaining portion of the statement on the comma seperator. If the comma is found, one of two cases will be returned i.e.: (register, value) or @register, value). These are stored in the extra variable. We then add the extra fields to the fields variable and assign mnemonic the value in fields[0] which contains the instruction mnemonic.

Next, using the value of ‘nofields’ which indicate what we should expect next, we sort out the remaining fields and store their values into their perspective variables. In each case, we call parse functions which perform a specific sub-task.

Our first sub-task is to locate any register in the fields. At this point we know if a register exists it should be in fields[1]. So we pass this value to the parse_register() function shown below:

def parse_register(field: str) -> str:
    """ Examples:
            R2
            @R3
            R2, 0x1F
    """
    register = field.upper()
    if register.__contains__('R'):
        pos = field.find('R')
        register = field[pos + 1:pos + 2]
    return register

The first thing we do is convert the register value to upper case. Then we check if the string contains ‘R’ and return the character following the ‘R’ which should be the register index value (a single digit between 0 and 7 inclusively). We then return that value to the caller.

It’s possible that we didn’t have a register. In this case, there may be a value in the fields[1] position. So we check if the call to parse_register() returned a value of an empty string. If the latter was returned, we try and parse a value using the function parse_value() passing in fields[1] as a parameter. This situation occurs with the branch instructions where the mnemonic is directly followed by the offset value for the jump.

def parse_value(val: str) -> str:
    """ Try to parse a value, which can be an integer
        in base 2, 8, 10, 16 or a register.

        Returns: A string representation of the integer
        value in base 10, or empty string if the value
        cannot be converted to an integer.
        On Error: return original value.
    """
    # convert value to int from various bases
    if isinstance(val, str):
        # Get any possible prefix
        val = val.strip(' ')
        base = val[0:2]
        if val.isnumeric():
            return str(int(val, 10))
        if base == '0x':
            return str(int(val, 16))
        elif base == '0c':
            return str(int(val, 8))
        elif base == '0b':
            return str(int(val, 2))
        elif val.__contains__('R'):
            return ''
        else:
            return val

As you can see above the parse_value() function must handle many types of values. It first strips off any prefix to the value and tests if this matches any of the supported numerical base indicators. If a match is found the value is converted to an integer and then cast into a string and returned to the caller.

Next, we call parse_address_mode() passing in all the fields as we may need more than one to identify the addressing mode. The parse_address_mode() function can only be understood in the context of the opcode_table. So refer to the table as you gork this code:

def parse_address_mode(fields: list) -> str:
    """ Example inputs:
                ['HALT']                    : implicit mode
                ['BRA', '0x1F']             : immediate mode
                [LD, R2]                    : register
                [STD, @R3]                  : indirect
                ['SET', 'R0,', '0xFFFE']    : direct
                ['BRC' 'end']               : offset
    """
    numfields = len(fields)
    if numfields == 1:
        return 'implicit'
    elif numfields == 2:
        if fields[0] in directives:
            return 'immediate'
        if fields[1].__contains__('@R'):
            return 'indirect'
        elif fields[1].__contains__('R') and fields[1].__contains__(','):
            return 'direct'
        elif fields[1].__contains__('R'):
            return 'register'
        elif parse_value(fields[1]).isnumeric():
            return 'immediate'
        elif parse_value(fields[1]).isalnum() or parse_value(fields[1]).isalpha():
            return 'offset'
    elif numfields == 3 and parse_value(fields[2]).isnumeric():
        return 'direct'
    elif numfields == 3 and '(' in fields[2] and ')' in fields[2]:
        return 'direct'
    else:
        raise ValueError('Expected numeric value got: {expected}'.format(expected=fields[2]))

The parse_address_mode() function uses the data contained in the fields to sort out the addressing mode of the instruction and return it to the caller, parse_opcode() which then stores this value in the mode variable. 

Next we use the mnemonic value to get the instruction data from the opcode_table and then use the mode variable to get the proper opcode value and instruction format.

If all has gone well, we return a tuple containing the value, register, mode, mnemonic, variables and the object code taken from the opcode_table.

At this poiont our first pass is complete. We have all the data necessary to assemble our instruction code with the possible exception of any forward declared symbol. 

We have one last issue before we can try this out. Our opcode_table has method names in some of the fields. We need to implemnt these helper methods:

All of these methods are pretty strainght forward. They simply take the value from the source code and separe it out into high bytes and low bytes in the case of VALUE_H, VALUE_L, LABEL_H, and LABEL_L. The OFFSET function calculates the offset from the current position.

# Functions used in the creation of object code (used in the table below)
def VALUE_L(pc: int, value):
    val = value_to_int(value) & 0xff
    return str(val & 0xff)


def VALUE_H(pc: int, value: int) -> int:
    val = (value_to_int(value) & 0xff00) >> 8
    return str(val & 0xff)
 
  
def LABEL_L(pc, value):
    print(f'LABEL_L PC: {pc}, value: {value}')
    return (value) & 0xff


def LABEL_H(pc, value):
    print(f'LABEL_H PC: {pc}, value: {value}')
    return ((value) & 0xff00) >> 8


def OFFSET(pc, value):
    print(f'OFFSET: {value}')
    return ((value - pc)) & 0xff


def value_to_int(value):
    # convert value to int from various bases
    if isinstance(value, str):
        if value[1:3].lower() == '0x':
            return int(value, 16)
        elif value[1:3].lower == '0c':
            return int(value, 8)
        elif value[1:3].lower() == '0b':
            return int(value, 2)
        else:
            print('Invalid value.')
    elif isinstance(value, int):
        return value
    # else:
    #     raise ValueError("Expected numerical value, got: {0}".format(value))


def register_to_int(reg):
    if isinstance(reg, str):
        if reg.startswith('@R'):
            return int(reg[2:], 16)
        elif reg.startswith('R') and reg.endswith(','):
            return int(reg[1:-1], 16)
        elif reg.startswith('R'):
            return int(reg[1:])
        else:
            raise ValueError("Cannot parse the register: '{0}'".format(reg))

PK, I think we’re ready to try this! Below is the final code. Note I made some adjustments to the code from the last installment so we could test what we’ve done so far. 

"""Sweet16-GP Assembler"""


# Exception used for errors
class AssemblyError(Exception):
    pass


# Functions used in the creation of object code (used in the table below)
def VALUE_L(pc: int, value):
    # print(f'VALUE_L value: {value}')
    #val = value_to_int(value) & 0xff
    #print(f'VALUE_L val: {val}')
    #return str(val & 0xff)
    pass


def VALUE_H(pc: int, value: int) -> int:
    val = (value_to_int(value) & 0xff00) >> 8
    return str(val & 0xff)


def LABEL_L(pc, value):
    return (value) & 0xff


def LABEL_H(pc, value):
    return ((value) & 0xff00) >> 8


def OFFSET(pc, value):
    return ((value - pc)) & 0xff


def value_to_int(value):
    # convert value to int from various bases
    if isinstance(value, str):
        if value[1:3].lower() == '0x':
            return int(value, 16)
        elif value[1:3].lower == '0c':
            return int(value, 8)
        elif value[1:3].lower() == '0b':
            return int(value, 2)
        else:
            print('Invalid value.')
    elif isinstance(value, int):
        return value
    # else:
    #     raise ValueError("Expected numerical value, got: {0}".format(value))


def register_to_int(reg):
    if isinstance(reg, str):
        if reg.startswith('@R'):
            return int(reg[2:], 16)
        elif reg.startswith('R') and reg.endswith(','):
            return int(reg[1:-1], 16)
        elif reg.startswith('R'):
            return int(reg[1:])
        else:
            raise ValueError("Cannot parse the register: '{0}'".format(reg))


opcode_table = {
    'HALT': {
        'implicit': ['0x00'],
    },
    'BRA': {
        'immediate': ['0x01', VALUE_L],
        'offset': ['0x01', OFFSET],
    },
    'BRC': {
        'immediate': ['0x02', VALUE_L],
        'offset': ['0x02', OFFSET],
    },
    'BRZ': {
        'immediate': ['0x03', VALUE_L],
        'offset': ['0x03', OFFSET],
    },
    'BRN': {
        'immediate': ['0x04', VALUE_L],
        'offset': ['0x04', OFFSET],
    },
    'BRV': {
        'immediate': ['0x05', VALUE_L],
        'offset': ['0x05', OFFSET],
    },
    'BSR': {
        'immediate': ['0x06', VALUE_L, VALUE_H],
        'offset': ['0x06', LABEL_L, LABEL_H],
    },
    'RTS': {
        'implicit': ['0x07'],
    },

    'SET': {
        'direct': ['0x08', VALUE_L, VALUE_H],
    },
    'LD': {
        'register': ['0x10'],
        'indirect': ['0x20'],
    },
    'ST': {
        'register': ['0x18'],
        'indirect': ['0x28'],
    },
    'LDD': {
        'indirect': ['0x30'],
    },
    'STD': {
        'indirect': ['0x38'],
    },
    'POP': {
        'indirect': ['0x40'],
    },
    'STP': {
        'indirect': ['0x48'],
    },
    'ADD': {
        'register': ['0x50'],
    },
    'SUB': {
        'register': ['0x58'],
    },
    'MUL': {
        'register': ['0x60'],
    },
    'DIV': {
        'register': ['0x68'],
    },
    'AND': {
        'register': ['0x70'],
    },
    'OR': {
        'register': ['0x78'],
    },
    'XOR': {
        'register': ['0x80'],
    },
    'NOT': {
        'register': ['0x88'],
    },
    'SHL': {
        'register': ['0x90'],
    },
    'SHR': {
        'register': ['0x98'],
    },
    'ROL': {
        'register': ['0xA0'],
    },
    'ROR': {
        'register': ['0xA8'],
    },
    'POPD': {
        'indirect': ['0xE0'],
    },
    'CPR': {
        'register': ['0xE8'],
    },
    'INC': {
        'register': ['0xF0'],
    },
    'DEC': {
        'register': ['0xF8'],
    },
}

def parse_value(val: str) -> str:
    """ Try to parse a value, which can be an integer
        in base 2, 8, 10, 16 or a register.

        Returns: A string representation of the integer
        value in base 10, or empty string if the value
        cannot be converted to an integer.
        On Error: return original value.
    """
    # convert value to int from various bases
    if isinstance(val, str):
        # Get any possible prefix
        val = val.strip(' ')
        base = val[0:2]
        if val.isnumeric():
            return str(int(val, 10))
        if base == '0x':
            return str(int(val, 16))
        elif base == '0c':
            return str(int(val, 8))
        elif base == '0b':
            return str(int(val, 2))
        elif val.__contains__('R'):
            return ''
        else:
            return val


def parse_address_mode(fields: list) -> str:
    """ Example inputs:
                ['HALT']                    : implicit mode
                ['BRA', '0x1F']             : immediate mode
                [LD, R2]                    : register
                [STD, @R3]                  : indirect
                ['SET', 'R0,', '0xFFFE']    : direct
                ['BYTE' '0x1f']             : immediate
                ['WORD' '0x3BFC']           : immediate
                ['STRING' 'This is a test'] : immediate
                ['BRC' 'end']               : offset
    """
    numfields = len(fields)
    if numfields == 1:
        return 'implicit'
    elif numfields == 2:
        # if fields[0] in directives:
        #     return 'immediate'
        if fields[1].__contains__('@R'):
            return 'indirect'
        elif fields[1].__contains__('R') and fields[1].__contains__(','):
            return 'direct'
        elif fields[1].__contains__('R'):
            return 'register'
        elif parse_value(fields[1]).isnumeric():
            return 'immediate'
        elif parse_value(fields[1]).isalnum() or parse_value(fields[1]).isalpha():
            return 'offset'
    elif numfields == 3 and parse_value(fields[2]).isnumeric():
        return 'direct'
    elif numfields == 3 and '(' in fields[2] and ')' in fields[2]:
        return 'direct'
    else:
        raise ValueError('Expected numeric value got: {expected}'.format(expected=fields[2]))


def parse_register(field: str) -> str:
    """ Examples:
            R2
            @R3
            R2, 0x1F
    """
    register = field.upper()
    if register.__contains__('R'):
        pos = field.find('R')
        register = field[pos + 1:pos + 2]
    return register


def parse_opcode(line: str, symbols) -> tuple:
    """ Break the line into its constituent parts.
        Locate any labels and store their definition.
        Then locate the instruction's addressing mode,
        and stores it in mode. Calculate the instruction's
        actual opcode value.

        Returns: tuple(value, register, mode, objcode) where
        value is a string to be evaluated in the second pass.
        register is the register value or empty string if no
        register exists. "mode" is the addressing mode and
        objcode is a dict containing the base opcode value,
        and a list of functions needed to process the
        instruction
    """
    fields = line.split(None, 1)
    nofields = len(fields)
    if nofields > 1:
        extra = fields[1].split(',')
        if len(extra) > 1:
            fields[1] = extra[0]
            fields.extend(extra[1:])
    nofields = len(fields)
    mnemonic = fields[0]
    register = ''
    value = ''

    """ Examples:
            HALT
            BRA 0x1F
            SET R0, 0xFFFE
            LD R2
            STD @R3
    """
    # Get register and value
    if nofields > 1:
        register = parse_register(fields[1])

    if register == '' and nofields > 1:
        value = parse_value(fields[1])

    if nofields > 2:
        register = parse_register(fields[1])
        value = parse_value(fields[2])

    # Get address mode
    mode = parse_address_mode(fields)

    # Get all addressing modes for this mnemonic
    opcodemodes = opcode_table.get(mnemonic)
    if not opcodemodes:
        raise AssemblyError("Unknown opcode '{}'".format(mnemonic))

    # Get the address mode used in the instruction
    objcode = opcodemodes.get(mode)
    if not objcode:
        raise AssemblyError("Invalid addressing mode '{0}' for {1}".format(mode, mnemonic))

    return value, register, mode, mnemonic, list(objcode)


def parse_lines(lines, symbols):
    """ Determine mnemonic, register, value, and address mode"""
    for lineno, line in enumerate(lines, 1):
        # Handle labels
        label, *colon, statement = line.rpartition(":")
        try:
            # parse the line into ir (intermediate representation)
            data = lineno, label, parse_opcode(statement, symbols) if statement \
                else (None, None, None, None, None)
            yield data
        except AssemblyError as e:
            print("{0:4d} : Error : {1}".format(lineno, e))


def assemble(lines, lc=0):
    """Breaks the line into its constituent parts.
        it then locates the instruction's addressing mode,
        stores it in mode, and calculates the instruction's
        actual opcode value and length. """
    objcode = []
    symbols = {}

    # Pass 1 : Parse instructions and create intermediate code
    for lineno, label, (value, register, mode, mnemonic, icode) in parse_lines(lines, symbols):
        # Try to evaluate numeric labels and set the lc (location counter)
        if label:
            try:
                lc = int(eval(label, symbols))
            except (ValueError, NameError):
                symbols[label] = lc

        # Store the resulting object code for later
        # expansion and adjust the location counter lc.
        if icode:
            objcode.append((lineno, lc, value, register, mode, mnemonic, icode))
            lc += len(icode)
    return objcode


def replace_register_names(line):
    """ Replace register names with 'R' + register index.
        Example: ACC becomes R0. STATUS becomes R6."""
    line = line.replace('ACC', 'R0')
    line = line.replace('RETSTACK', 'R4')
    line = line.replace('COMP', 'R5')
    line = line.replace('STATUS', 'R6')
    line = line.replace('PC', 'R7')
    return line


def strip_lines(lines):
    """ Takes a sequence of lines and strips comments and blank lines."""
    for line in lines:
        comment_index = line.find(";")
        if comment_index >= 0:
            line = line[:comment_index]
        line = line.strip()
        line = replace_register_names(line)
        yield line


if __name__ == '__main__':
    text = ';This is a comment\n' \
           'start:  SET  ACC, 0xFFDE  ; This is also a comment\n' \
           'end:    HALT  ; end of program'
    lines = text.split('\n')

    # Remove comments and blank lines
    lines = strip_lines(lines)
    print(assemble(lines))

If you run the above code you should get an output like the following:

 

sweet16-articles-code/articles/part-07/assembler_02.py
 [
  (2, 0, '65502', '0', 'direct', 'SET', ['0x08', , ]), 
  (3, 3, '', '', 'implicit', 'HALT', ['0x00']) 
 ]

As you can see we have the intermediate representation for two instructions. All the info we will need to assemble these instructions into actual machine code. Reading the tuples the first value (2) is the line number from the source. The second value (0) is the location counter at the start of the instruction. The next value (65502) is the value provided in the instruction, followed by the register index (0), the addressing mode (direct), and the mnemonic. The final fields holds the opcode information from the opcode_table.  Try this out on several instructions and see that the results make sense. Write some unit test for the various functions and for the assemble as it stands. 

This has been a long post. Next time we will tackle the second pass of our assembler. Until then, keep coding!

 

Simple Graphics in Python – Part 3

by SysOps 0 Comments

Last time we left off discussing Color in John Zelle’s graphics.py library. If you didn’t catch part 1 and 2 of this series I recommend you read those parts first and then return here. You can find part 1 here: Simple Graphics in Python.

We’re almost done going over the library from a user standpoint. However, in the future I may discuss how the library works if there is enough interest. This time, we’ll be discussing window updating and animations. We’ll develop a few sample apps and have some fun. My intention here isn’t to develop full fledged apps but, rather give you a starting point for your own apps using the graphics.py library. So let’s get started.

Window Updates

The graphics.py library usually handles window updates for you anytime an object that has been drawn to the window changes. Under some circumstances it may be necessary to force a window update. For example, when using the library from some interactive shells. The window may be forced to update using the update() method on the GraphWin object. This will redraw all the items in the window.

The window auto update feature is great for simple graphics. However, as your scenes become more complex you may want to take charge and start updating the window when it bests fits your program’s schedule. This may become necessary when you are drawing many, many items to the window. You can improve efficiency by updating the window only after all items have been drawn. If you want to turn off the auto update window feature you can do so when you create the window by passing autoflush=False as in:

win = GraphWin("Window Title", 400, 400, autoflush=False)

This will disable the auto update feature and you’ll be responsible for calling win.update() when you desire to redraw all the objects in the window. Here’s an example:

"""
Prog:   ex-11_02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library turning off the 
        window's auto update feature and 
        calling win.update() yourself.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win = GraphWin("Rectangles", 640, 480, autoflush=False)
    
    points = []
    for i in range(1):
        for p in range(0, randint(4, 20)):
            points.append(Point(randint(0,639), randint(0,479)))
        
        r = randint(0,255)
        g = randint(0,255)
        b = randint(0,255)

        poly = Polygon(points)
        poly.setOutline(color_rgb(r, g, b))
        poly.draw(win)

    # First mouse click adds a polygon
    points[0] = win.getMouse()  
    points[-1] = points[0] 
    poly2 = Polygon(points)
    poly2.setOutline('white')
    poly2.draw(win) 

    # Second mouse click should show the new polygon
    win.getMouse()
    update()

    # Thrid mouse click should close the window.
    win.getMouse()
    win.close()

main()

When I read the docs and implemented this program I expected that updates would only occur when the update() method was called. When this didn’t work as expected I re-read the docs and when I still couldn’t understand what was happening, I emailed John. He was kind enough to respond and set me straight about my misunderstandings. After reading his response and once again, re-reading the docs, I realized I had read, but ignored one statement in the docs. This was the cause of misunderstanding. Here’s the line I skimmed over and missed the details:

Now changes to the objects in win will only be shown when the graphics system has some idle time or when the changes are forced by a call to update().

When I read this I walked away with the impression that updates only occur when update() was called if you passed autoflush=False. However, the statement clearly says that auto-updates still occur when the system has idle time. So update is only useful if you have a blocking operation that keeps the auto-update from running.

John pointed out that in the code above, the getMouse() method calls are blocking methods but that he coded them to call update() and force drawing on the window. So my calls to getMouse don’t actually work as commented in the code above. In fact, they force an update to the window.

So with that insight let’s see if we can write a sample that actually demonstrates the use of update. Create the example below:

"""
Prog:   ex-11_03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library turning off the 
        window's auto update feature and 
        calling win.update() yourself.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win1 = GraphWin("Rectangles", 640, 480, autoflush=False)
    #win2 = GraphWin("Rectangles", 640, 480)
    
    points = []
    for i in range(1):
        # Build up initial polygon
        for p in range(0, randint(4, 20)):
            points.append(Point(randint(0,639), randint(0,479)))
        
        r = randint(80,255)
        g = randint(0,255)
        b = randint(50,255)

        poly = Polygon(points)
        poly.setOutline(color_rgb(r, g, b))
        poly.draw(win1)

        # Loop to keep system busy
        # Since we are using a loop 
        # to delay the system you may need to increa
        max_iters = 999999999
        delay_frac = 33333333
        td = 0
        for j in range(0,max_iters):
            if j % delay_frac == 0:
                poly.undraw()
                poly = None
                r = randint(30,255)
                g = randint(30,255)
                b = randint(30,255)
                points.append(Point(randint(0,639), randint(0,479)))
                poly = Polygon(points)
                poly.setOutline(color_rgb(r, g, b))
                poly.draw(win1)
                print("Updated #", td)
                td += 1
                update() # We manually call update here, then delay again.
    
    
    # mouse click should show the new polygon
    # after busy loop completes
    win.getMouse()
    win.close()

main()

OK, with our new understanding of the autoflush=False option we’ll run the app above. You may need to adjust the value of max_iters and delay_frac as max_iters controls the total run-time of the delay loop and delay_frac controls the delay between updates during the loops.

Our program begins by creating a polygon and displaying it on the screen. Next, we enter a loop and stay in the loop for a very long time. This loop blocks the auto-update feature from updating the display. During the execution of the delay loop we check if we have made delay_frac (delay fraction) iterations since our last update. If so, the modulus expression will return 0 and the if statement will evaluate to true and we enter the if clause. Next, we erase and destroy the original polygon, and generate a new polygon using the points of last polygon with one new point added for good measure. Adding a point allows us to see the shape changed on update. The important thing to understand here is that the object isn’t being drawn to the window until we reach the update line at the bottom of the if clause.

However, if we were to forego the delay loop the auto-update feature would take over and draw the polygon when the system became idle or a method that itself (like the getMouse()) calls update() is called.

One last thing to know about the update() method is that it can take an integer parameter for the desired frame-rate. If you pass a desired frame-rate to update() it as in:

update(30)

It will update the window at this rate.

Animations

While it’s possible to use the graphics library for a GUI (Graphical User Interface), most NooBs will want to do something a bit more entertaining with it. I’m not going to tech game development here but I thought I would toss out a few example apps that I’ll intentionally leave unfinished so you, the reader, can have fun adding features and completing the demo apps.

There are a few ways to accomplish animation on a computer. The most often used is motion animation where an object is moved into it’s new location, then drawn, then erased and moved again. This cycle is known as the animation loop, or if you’re a gamer, the game loop.

Handball

Our first animation is a simple Pong-like game (remember these apps will be unfinished and incomplete) that simply draws a circle and a rectangle on the screen then moves them around the screen.

Screen Shot of the Handball App

We will use an OOP (Object Oriented Programming) approach for the Handball app.

#!/usr/bin/env python3

"""
Prog:   ex-12_01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to do simple
        animation of a pendulum.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *

width = 640
height = 480


class Paddle():
    def __init__(self, x, y, win):
        self.x = x
        self.y = y
        self.w = 10
        self.h = 50
        self.win = win
        self.rect = Rectangle(Point(self.x, self.y), Point(self.x+self.w, self.y+self.h))
        self.rect.setFill('white')
    
    def getX(self):
        return self.rect.p1.getX()

    def getY(self):
        return self.rect.p1.getY()

    def getW(self):
        return self.w

    def getH(self):
        return self.h
        
    def move(self, xspeed, yspeed):
        self.rect.move(xspeed, yspeed)

    def draw(self):
        self.rect.draw(self.win)

    def undraw(self):
        self.rect.undraw()

  
class Ball():
    def __init__(self, x, y, win):
        self.x = x
        self.y = y
        self.xspeed = 3
        self.yspeed = 1
        self.r = 10
        self.win = win
        self.cir = Circle(Point(self.x, self.y), self.r)
        self.cir.setFill('white')

    def set_speed(self, xspeed, yspeed):
        self.xspeed = xspeed
        self.yspeed = yspeed
    
    def move(self):
        self.cir.move(self.xspeed, self.yspeed)
        p1 = self.cir.getP1()
        self.x = p1.getX()
        self.y = p1.getY()
        
    def draw(self):
        self.cir.draw(self.win)

    def undraw(self):
        self.cir.undraw()

    def check_collision(self, pad):
        print("Xspeed: ", self.xspeed, "Yspeed: ", self.yspeed) 
        xbound = self.within_x_bounds(pad)
        ybound = self.within_y_bounds(pad)

        if xbound and ybound:
            if xbound:
                self.xspeed = -self.xspeed
            if ybound:
                self.yspeed = -self.yspeed
            self.move()
            return True

        return False
        

    def within_x_bounds(self, pad):
        if self.xspeed < 0:
            if (self.x < pad.getX() + pad.getW()) and (self.x > pad.getX()):
                return True
            else:
                return False
        else:
            if (self.x + self.r >= pad.getX()) and (self.x <= pad.getX() + pad.getW()):
                return True
            else:
                return False
    
    def within_y_bounds(self, pad):
        if self.y+self.r >= pad.getY() and self.y <= pad.getY()+pad.getH():
            return True
        else:
            return False
    
    def check_edges(self, width, height):
        p1 = self.cir.getP1()
        if p1.getX() < 0 or p1.getX()+self.r > width:
             self.xspeed = -self.xspeed;
        if p1.getY() < 0 or p1.getY()+self.r > height:
            self.yspeed = -self.yspeed;
  
      
def main():
    win = GraphWin("Handball", width, height)
    win.setBackground('black')

    xspeed = 0.1
    yspeed = 0.1

    # initial placement of paddle  
    pad = Paddle(10, (height/2)-25, win) 
    # Draw paddles.
    pad.draw()

    ball = Ball(width/2, height/2, win)
    ball.set_speed(xspeed, yspeed)
    ball.draw()
   
    while 1:
        # get imput if any
        k = win.checkKey()
        if k == 'a':
            if pad.getY() < 0:
                pad.move(0, 0) 
            else:
                pad.move(0, -20)
            print('Pad1: Move Up', pad.getX(), pad.getY())

        if k == 'z':
            if pad.getY() > height - 50:
                pad.move(0, 0)
            else:
                pad.move(0, 20)
            print('Pad1: Move down', pad.getX(), pad.getY())

        ball.check_edges(width, height)
        ball.move()

        if ball.check_collision(pad):
            print("Ball hit paddle")

    win.getMouse()
    win.close()


main()

This may not be the most efficient implementation however, it is only meant to provide you with some inspiration for creating your own apps by demonstrating what can be accomplished using the graphics.py library.

If you scan the code you quickly see that we have a Paddle calls, a Ball class, and a main function. Our Paddle object encapsulates properties (data) and methods (actions) our paddle can take. Our paddle needs to keep track of it’s position (x,y) and size (w,h). When we create a Paddle object from the class (a class is a blueprint for the object we want to create) we pass in these values along with the window we want the paddle to draw itself on. We save the window for future use as we will always draw the paddle to the same window. So saving it here simplifies our code and we no-longer have the need to pass the window each time we call draw() on the paddle.

When a paddle is instantiated (an object is created from the class), Python calls the __init__() method. In this method you place all the code that you need to run to set things up for use. So we create the rectangle that will represent our paddle on the screen. We also set the fill color on the paddle to white. Our paddle is now ready for use.

Often you’ll need to have access to the state of an object. Later, we’ll need to be able to determine if the ball hits our paddle so, we need access to the location and size of our paddle. Do enable this we provide accessor methods getX(), getY(), getW(), getH(). These return the paddles x, y, width, and height respectively.

Our paddle also needs to move up and down so we can hit the ball as it bounces across the court. So, well need a move() method. There may be times we want to move at different speeds. So will pass in the xspeed and yspeed for our paddle. You may be wondering why we need the xspeed. Truly we don’t. We could just hard code the xspeed in our class code. But that would restrict us to moving only in the Y plane. Yes, it’s true that the paddle in Pong moves only in the Y play (up and down). But think how much fun it would be to animate the paddle to shake when the ball hits it. Here, we would need access to the yspeed to accomplish this. Including it also opens the class up for reuse. For example, suppose you want to use the paddle in a falling object game. If we didn’t include the yspeed here, you wouldn’t be able to.

In almost all motion animations each object will need to complete the three tasks of the animation loop, Move, Draw, Erase, Repeat… The graphics library actually takes care of this for use in the move() method of the various shape objects. So we really don’t need to worry about it. But you do need to know it’s happening under the hood.

we’ll add a draw() method to our paddle. Here we only need to call draw on the rectangle that represents our paddle on on the display. We may also need an erase method at some point. So, we’ll include it here and again however, we’ll call it undraw() to stay consistent with the library methods. All we need to do in the undraw() method is to call it’s namesake on the rectangle that represents our paddle.

The Ball class is a bit more complicated. Mostly because we encapsulated the logic of what to do when the ball comes in contact with another object. For example, if the ball hits the edge of the screen or the paddle. The balls move method is a bit different than the paddle’s move method. This is because it is expected that once the ball is moving it will keep moving. Also, we don’t want to have to changed the balls direction ourselves. We want it to include this action when it hits an object so it bounces off on it’s own. So in the Ball class we provide an xspeed and yspeed and set default values for them. Our ball is represented by a circle on the screen. So we have to create a circle and save it. We laos set the fill color in the __init__() method.

We may need to change the ball’s speed so we include a set_speed() method. We will also need to know when the ball has hit the edge of the court. This is handled in the check_edges() method. Here you’ll need to pass in the courts with and height. It is assumed that the upper right corner of the court is (0,0) and all calculations make this assumption.

The check_collision() method is passed the paddle object to test for collision with the ball. The ball object includes two helper methods, within_x_bounds() and within_y_bounds() to check if the ball is within the bounds of the paddle object.

To make this a complete game you need to add scoring and allow the ball to reset and be re-served if it passes the paddle. You can also use this as the basic frame work for Pong by adding another paddle and additional input handling for another player. Just a hint if you try this, google keyboard input methods for python before you attempt this. As, the current approach wont report multiple key presses at once. Their are solutions but I’ll leave that as an exercise for the reader.

Simulations

Games and GUIs aren’t the only things that graphics can help with. Graphics are often used to convey information about some chemical or mathematical process. Let’s take a simple case, that of calculating pi. It is well known that PI can be estimated to surprising accuracy by randomly throwing darts at art board. OK, so it’s a bit more complex than that but, only a little. First, what we really need is a circle inside a square. The circle’s diameter must fit snugly inside the square. More precisely the diameter of the circle must equal the length of one side of the square.

The logic is simple: If the circle’s diameter is equal to the square’s length, than the area of the circle should be equal to: (area of the square / area of circle)*4. To learn more about this you can checkout this link: https://www.youtube.com/watch?v=M34TO71SKGk

We can draw circles, squares, and points (to represent darts) using the graphics.py library. So all we need to do is draw a circle inside a square and throw darts at it, then calculate the ratio of darts that landed in the circle to the total number of darts thrown. We will simply plot random points for our darts and keep track of how many we throw and where they landed. Let’s see how we might do this in python:


#!/usr/bin/env python3
"""
Prog:   ex-13.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to visualize the
        process of estimating pi by randomly
        throwing darts.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from random import *
from math import *

width = 400
height = 400
center = width/2
r = width/2

# Find the deststance between two points
def dest(x1,y1, x2,y2):
    return sqrt((x1 - x2)**2 + (y1 - y2)**2)


def main():
    win = GraphWin("Pi Estimation", width, height)
    # Draw a square
    sq = Rectangle(Point(0,0), Point(width-1,height-1))
    sq.setOutline("blue")
    sq.draw(win)
    # Draw a circle fitting the square
    c = Circle(Point(center,center), r)
    c.setOutline("white")
    c.draw(win)

    darts_thrown = 0
    darts_in_circle = 0
    best = 0

    estimate = 0
    best_estimate = 0
    for i in range(1,100000):
        x = randint(0, 400)
        y = randint(0, 400)
        p = Point(x,y)
        darts_thrown += 1
                
        # Is are point in the circle?
        if(dest(center,center, x, y) < r):
            darts_in_circle += 1
            p.setFill(color_rgb(220,200, 120))
            p.draw(win)
        else:
            p.setFill(color_rgb(127, 200, 127))
            p.draw(win)
        
        if i % 3000 == 0:
            estimate = (darts_in_circle/darts_thrown)*4
            if abs(pi - estimate) < abs(pi - best_estimate):
                best_estimate = estimate
            
            print("Iteration: ", i, " Estimated PI: ", best_estimate)

    print("Done!")

    win.getMouse()
    win.close()


main()

If you run this code you should get a printed output of something like this:

Iteration: 3000 Estimated PI: 3.0893333333333333
Iteration: 6000 Estimated PI: 3.0893333333333333
Iteration: 9000 Estimated PI: 3.089777777777778
Iteration: 12000 Estimated PI: 3.0936666666666666
Iteration: 15000 Estimated PI: 3.1018666666666665
Iteration: 18000 Estimated PI: 3.110222222222222
Iteration: 21000 Estimated PI: 3.1125714285714285
Iteration: 24000 Estimated PI: 3.1161666666666665
Iteration: 27000 Estimated PI: 3.1161666666666665
Iteration: 30000 Estimated PI: 3.1161666666666665
Iteration: 33000 Estimated PI: 3.1161666666666665
Iteration: 36000 Estimated PI: 3.1172222222222223
Iteration: 39000 Estimated PI: 3.12174358974359
Iteration: 42000 Estimated PI: 3.12174358974359
Iteration: 45000 Estimated PI: 3.1226666666666665
Iteration: 48000 Estimated PI: 3.12475
Iteration: 51000 Estimated PI: 3.124941176470588
Iteration: 54000 Estimated PI: 3.124941176470588
Iteration: 57000 Estimated PI: 3.124941176470588
Iteration: 60000 Estimated PI: 3.124941176470588
Iteration: 63000 Estimated PI: 3.124941176470588
Iteration: 66000 Estimated PI: 3.1267878787878787
Iteration: 69000 Estimated PI: 3.1267878787878787
Iteration: 72000 Estimated PI: 3.1267878787878787
Iteration: 75000 Estimated PI: 3.1267878787878787
Iteration: 78000 Estimated PI: 3.1267878787878787
Iteration: 81000 Estimated PI: 3.1267878787878787
Iteration: 84000 Estimated PI: 3.1267878787878787
Iteration: 87000 Estimated PI: 3.1267878787878787
Iteration: 90000 Estimated PI: 3.1267878787878787
Iteration: 93000 Estimated PI: 3.1267878787878787
Iteration: 96000 Estimated PI: 3.1267878787878787
Iteration: 99000 Estimated PI: 3.1267878787878787
Done!

I ran this program several times and the best I did was 3.1419. Which is pretty good given the fact that our random number generator is actually a pseudo random number generator. I also believe that the math library in python may be rounding our calculations. Using a more precise math library would improve the estimate. However, this app is only meant to demonstrate the process. So, I’ll leave implementing a more precise version up to the reader.


Screen Shot of Pi Estimator App

Running the application longer with more dart throws will improve the estimation of PI.

Multiple Window

The graphics.py library allows you to have multiple windows. This can be handy for both GUIs and data visualization. You could for example display the plot of darts in a PI estimation program in one window while plotting the error on a graph in another window.

You might wonder why you would ever need more than one window. Well, how often do you use a drop down menu? The drop down menu is actually a small window with a list of items that is placed over the main window. Dialog boxes, popups, etc… are all windows. So being able to create additional windows comes in very handy for GUI applications. However, other types of applications can make use of multiple window. Take our PI estimating application above. We could use an additional window to plot the standard deviation of our a current estimate. Using multiple windows you can show many plots at the same time. This would allow the user to correlate the information in the various plots.

I’m going to show you a simple demo that is once again, an incomplete game. This game is a two player version of Battleship. It has several issues left for you to resolve. however, it does demonstrate the use of two windows being used in a single application. The code here is a bit longer than our other applications and I would say this code is in a pre-alpha state. It is only meant to ignite you imagination and give you a base from which to work to complete the game.

I’m sure I don’t have to explain how Battleship is played. However, if you need and explanation, google “battleship game” and you’ll find a wikipedia article on it. Let’s see some code:


#!/usr/bin/env python3
"""
Prog:   ex-14.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library and the use of
        multiple windows in a single app.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from random import *
from math import *

width = 400
height = 400



class Board():

    def __init__(self, title, width, height):
        self.xsize = 10
        self.ysize = 10
        self.w = width
        self.h = height
        self.grid = []
        self.win = GraphWin(title, width, height)
        self.vessels = []


    # returns pixels per division
    def xdiv(self):
        xdiv = self.w / self.xsize
        return xdiv


    # returns pixels per division
    def ydiv(self):
        ydiv = self.h / self.ysize 
        return ydiv


    def rowcol_to_xy(self, r, c):
        y = int(r * self.ydiv())
        x = int(c * self.xdiv()) 
        return (x, y)


    def rowcol_to_point(self, r, c):
        p = self.rowcol_to_xy(r, c)
        return Point(p[0], p[1])


    def xy_to_rowcol(self, x, y):
        r = int(y / self.ydiv())
        c = int(x / self.xdiv())
        return (r, c)
    

    # return the coordinates in pixels for
    # the center of the cell at (row, col)
    def center_xy(self, r, c):
        # calc (x,y) position of upper left
        # corner of cell at (r,c)
        xy1 = self.rowcol_to_xy(r, c)
        # Calculate lower right corner
        xy2 = self.rowcol_to_xy(r+1, c+1)
        
        # find the middel of the cell
        dx = (xy2[0] - xy1[0]) - self.xdiv() / 2
        dy = (xy2[1] - xy1[1]) - self.ydiv() / 2
        cx = dx + xy1[0]
        cy = dy + xy1[1]

        return (cx, cy)


    def dist(self, x1, y1, x2, y2):
        return sqrt(((x2-x1)**2) + ((y2 - y1)**2))


    # draws the grid of cells on the board
    def draw(self):
        # Expects (0,0) to be located in the upper left
        xdiv = self.w / self.xsize
        ydiv = self.h / self.ysize
        for i in range(0, self.w, int(xdiv)):
            l = Line(Point(i, 0), Point(i, self.h))
            l.draw(self.win)

        for j in range(0, self.h, int(ydiv)):
            l = Line(Point(0, j), Point(self.w, j))
            l.draw(self.win)


    # place a vessel on the board
    def place(self, vessel):
        plX = vessel.row * self.ydiv()
        plY = vessel.col * self.xdiv()
        if vessel.rect == None:
            rowcol = self.rowcol_to_xy(vessel.row, vessel.col)
            x1 = rowcol[0]
            y1 = rowcol[1]
            if vessel.horz:
                x2 = x1 + (vessel.length * self.xdiv())
                y2 = y1 + self.ydiv()
            else:
                y2 = y1 + (vessel.length * self.ydiv())
                x2 = x1 + self.xdiv()
            vessel.rect = Rectangle(Point(x1, y1), Point(x2, y2))
        vessel.rect.setOutline(color_rgb(127,220,127))
        vessel.rect.draw(self.win)


    # tests to see if the vessels on this board
    # have been hit by the shot taken, and call
    # draw_hit() to mark the shot with a red X 
    # in the cell where it landed.
    def hit(self, loc):
        col = int(loc.getX() / (self.w / self.xsize))
        row = int(loc.getY() / (self.h / self.ysize))
        self.draw_hit(row, col)


    # draws the actual red X, called by hit()
    def draw_hit(self, row, col):
        xy1 = self.rowcol_to_xy(row, col)
        x1 = xy1[0]
        y1 = xy1[1]
        xy2 = self.rowcol_to_xy(row+1, col+1)
        x2 = xy2[0]
        y2 = xy2[1]
        
        p1 = Point(x1,y1)
        p2 = Point(x2, y2)
        p3 = Point(x1,y2)
        p4 = Point(x2, y1)

        l1 = Line(p1, p2)
        l2 = Line(p3, p4)
        l1.setOutline('red')
        l2.setOutline('red')
        l1.draw(self.win)
        l2.draw(self.win)


    # Use to mark the shooter's board
    # for shots taken. So the player may
    # know where they have already shot
    def mark(self, r, c):
        c = self.center_xy(r, c)
        print("Center of mark, x: " + str(c[0]) + ", y: " + str(c[1]))
        pc = Point(c[0], c[1])
        cir = Circle(pc, int(self.xdiv()/2))
        cir.setOutline(color_rgb(50, 50, 200))
        cir.draw(self.win)



# Simple vessel class
class Vessel():

    def __init__(self, name, row, col, length, place_horz):
        self.row = row
        self.col = col
        self.length = length
        self.horz = place_horz
        self.name = name
        self.hit_count = 0
        self.rect = None # created in board.place()

        if self.name == 'Carrier':
            self.makeCarrier()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Battleship':
            self.makeBattleship()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Cruiser':
            self.makeCruiser()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Submarine':
            self.makeSubmarine()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Destroyer':
            self.makeDestroyer()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        else:
            print('Illegal Vessel Type: "'+name+'" not defined')
            return None 


    def makeCarrier(self):
        if self.name != 'Carrier':
            return
        elif self.horz:
            self.col = randint(0, 4)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 4)


    def makeBattleship(self):
        if self.name != 'Battleship':
            return
        elif self.horz:
            self.col = randint(0,5)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 5)


    def makeCruiser(self):
        if self.name != 'Cruiser':
            return
        elif self.horz:
            self.col = randint(0,6)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 6)


    def makeSubmarine(self):
        if self.name != 'Submarine':
            return
        elif self.horz:
            self.col = randint(0,6)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 6)


    def makeDestroyer(self):
        if self.name != 'Destroyer':
            return
        elif self.horz:
            self.col = randint(0,7)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 7)


    def getName(self):
        return self.name


    def move(self, x, y):
        self.rect.move(x, y)


    def draw(self):
        self.rect.draw()   


    # Not Yet Implemented
    # Given a row, col value for
    # a shot, return true if the
    # vessel was hit by shot 
    def hit(self, r, c):
        return False
    


# Simple player class
class Player():

    def __init__(self, name, width, height):
        self.name = name
        self.board = Board(name, width, height)

        # Create fleet
        self.Carrier = Vessel('Carrier', randint(0,4), randint(0, 9), 5, True)
        self.Battleship = Vessel('Battleship', randint(0,5), randint(0,5), 4, False)
        self.Cruiser = Vessel('Cruiser', randint(0,4), randint(0,4), 3, True)
        self.Submarine = Vessel('Submarine', randint(1,4), randint(1,4), 3, True)
        self.Destroyer = Vessel('Destroyer', randint(1,4), randint(1,4), 2, True)


    def getName(self):
        return self.name


    def getMouse(self):
        return self.board.win.getMouse()


    # called when player should take turn
    def turn(self, board):
        loc = self.getMouse()
        board.hit(loc)
        rc = self.board.xy_to_rowcol(loc.getX(), loc.getY())
        self.board.mark(rc[0], rc[1])
        

    # Not Yet Implemented
    # Should test if the player's
    # entire fleet has been sunk,
    # if so, game over!
    def fleetSunk(self):
        return False
        pass


    def close(self):
        self.board.win.close()


    # Initialize fleet
    def draw(self):
        self.board.draw()
        self.board.place(self.Carrier)
        self.board.place(self.Battleship)
        self.board.place(self.Cruiser)
        self.board.place(self.Submarine)
        self.board.place(self.Destroyer)



def main():
    # Open game boards
    player1 = Player("Player 1", 400, 400)
    player2 = Player("Player 2", 400, 400)
    player1.draw()
    player2.draw()

    while ~player1.fleetSunk() and ~player2.fleetSunk():
        player1.turn(player2.board)
        player2.turn(player1.board)

    player1.getMouse()
    player1.close()
    player2.close()


main()
   

Looking over this code we can see it is really rather simple. In main() we create two players and call draw() on them. Next, we enter a while loop. This loop will loop forever as I left the fleetSunk() method unimplemented. It is hard coded to return false. I’ve left implementing this method up to the reader.

Within the loop we call turn on each player passing in the opponent’s game board. Each game board is responsible for calculating it’s own size, and completing all drawing operations on it’s grid.

The player.turn() method takes the opponent’s game board as a parameter and and after getting the mouse click location, passes that location to the opponent’s board.hit() method. Next, we convert the pixel (x,y) values to (row, column) values and pass those to our own board’s mark() method to draw a blue circle to indicate where we’ve taken shots. You could leave this set out or toggle it to make the game more challenging.

The board’s hit method is not implemented in this code and is also left as an exercise for the reader. However, it should take the row, column values passed in and determine if any of the vessels on it’s board have been hit. If so, it should mark that vessel as damaged and increment the vessel.hit_count. This should be done by calling the vessel’s hit() method. The vessel is sunk if the hit_count matches the vessel’s length.

The player’s fleetSunk() method should simply test if all the player’s vessels have been sunk and return true if they have.

I’m leaving the completion of this up to the readers. You’ll most likely want to add some type of scoring. You might even change the X draw for hit’s and the circle drawn as a marker, to an image of an explosion and a slash in the water respectively. You might also make the shooter’s board indicate whether the shot was a hit or a miss. You should have all the tools you need to implement these features. If you take a little time and analyze each class and each of it’s methods, you should have little trouble.

There is one issue that this code has I didn’t have time to correct. That is that the vessels are drawn at random locations and therefor often overlap each other. This isn’t good, as one shot can damage two vessels. This might be allowed if this were Angry Birds (two bird, one shot…). However, it’s Battleship! SO you’ll need to implement some method for ensuring that all vessels are placed in such a manner that they wont overlap. You can find one such solution here: https://stackoverflow.com/questions/3265986. This isn’t the only solution but it’s one that isn’t too hard to implement. Do note that one issue with this method is that everything is placed around a focal point that will never be occupied by a vessel. Effectively ensuring that the center call of the board will always be empty. This could be dealt with by shrinking the field for the purpose of the placement calculation and then randomly shifting it up or down one row.

Good luck! If you have questions of comments I’d enjoy hearing from you.

Simple Graphics in Python

by SysOps 0 Comments

For the beginning Python programmer, getting started with command line applications is the first step. At some point however, even a NooB will get bored with the terminal window and desire something more. Yet, many of the graphics and gui libraries for Python can be overwhelming for a novice. Prof. John Zelle saw the need for a simple graphics library for his students and went searching. He didn’t find anything he felt was suitable for the truly novice python student. So, he created a new graphics library to fill this niche’. John’s library was used in his book “Python Programming An Introduction to Computer Science”. Which has seen three editions so far.

John’s library is great for teaching and keeping students interested. It’s simple, lacks complex features, and is straight-forward to use. John has provided great documentation. However, many new pythonistas will desire examples.

In searching for examples I came across a short series on youtube.com.   That series, though well done and well received , was also incomplete. There had been sever posts requesting updates but those have yet to surface. For that reason I decided to do a series of articles showing some sample code and just what can be accomplished with John’s great little library. So here goes:

As a software developer you must get used to reading documentation. You can find the documentation for John’s library at : http://mcsp.wartburg.edu/zelle/python/. The best thing you can do for yourself is read this documentation. It is short, only 8 pages. Then come back here and follow along with the examples.

Before we can start drawing on a window, we need to get our environment set up. First, we need John’s graphics library. Second, we need to place it in the same folder as our code because it is not part of the python standard libraries. You can download the graphics.py file from John’s site here: http://mcsp.wartburg.edu/zelle/python/graphics.py. Once you have the library place it in the same folder as your project file. It is important that you place the “graphics.py” file in the same folder as your code file. Otherwise python may not be able to locate it. Also note that you will tkinter (python-tk) installed on your machine to use John’s library.


While Completing this series of blog posts, I had a minor issue. The issue turned out to be that I misunderstood the way autoflush=false worked, we’ll cover that much later in this series. I wrote John and he set me straight on the inner workings of the update() method, (also covered later). In his reply he informed me that his library can now be installed using pip. To install with pip run this command:

pip3 install –user http://bit.ly/csc161graphics

You can find more info here: http://www.pas.rochester.edu/~rsarkis/csc161/python/pip-graphics.html

Now create a folder for your projects. We will call this folder “graphics-zelle” and it will be placed in a “projects” folder.  Finally, we will create a sub-folder for each exercise we do. Create a file named ex-01.py and add the code below.

Ok, now let’s write some code!

Opening a window

To open a window that we can draw on requires two simple steps. First, import the graphics library file and second, instantiate a window object and assign it to a variable so we can access it later.

The first step is simple:

from graphics import *

Once we have imported the library we can instantiate the window by calling the GraphWin function and passing in the window title and it’s width and height. We’ll do this in function we’ll call main. Lastly, we need to call the main function to execute the program.


# Define a main function and instantiate the window object.
def main():
    window = GraphWin("Exercise-01", 640, 480)

main() # Call the main function 

Putting this all together we get:

"""
Prog:   ex-01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to open a window
        on the desktop.

Lic:    This code is placed in the public domain.

"""

# Creating a gui window requires only two steps
# Step 1: import the graphics.py library into 
# your local folder.

# Step 2: Instantiate a GraphWin object and assign
# it to a variable, passing the window title, and
# size parameters.

# Demo
from graphics import *

def main():
    win = GraphWin("Window Title", 640, 480)

    
main()

Now run ex-01.py. If you’re quick you’ll see a window pop open and immediately close. What’s going on here?

What’s going on is the program is doing exactly what was asked of it. It creates a window and then exits. So how do we stop this from happening? The easiest solution for the moment is get the program to wait for some input, until we close the window. We can add one line of code to ex-01.py to ask it to wait for a mouse click before exiting.


"""
Prog:  ex-01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to open a window
        on the desktop.

Lic:    This code is placed in the public domain.

"""

# Creating a gui window requires only two steps
# Step 1: import the graphics.py library into 
# your local folder.

# Step 2: Instantiate a GraphWin object and assign
# it to a variable, passing the window title, and
# size parameters.

# Demo
from graphics import *

def main():
    win = GraphWin("Window Title", 640, 480)
    win.getMouse()

main()

Now run the program. You should see the window open and stay open until you click the close button on the widow’s frame.  Clicking the window frame close button causes python to ask the OS and interpreter to clean up the window we created. This is a very poor practice. If we want the window destroyed, we should explicitly close it. We can do this by calling the close method on the window object. Modify your code as show below:


"""
Prog:   ex-01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to open a window
        on the desktop.

Lic:    This code is placed in the public domain.

"""

# Creating a gui window requires only two steps
# Step 1: import the graphics.py library into 
# your local folder.

# Step 2: Instantiate a GraphWin object and assign
# it to a variable, passing the window title, and
# size parameters.

# Demo
from graphics import *

def main():
    win = GraphWin("Window Title", 640, 480)
    win.getMouse()
    win.close()    

main()

This is much better. Now we can close the window simply by clicking on it. Still not a perfect solution but it at least allows the GraphWin object to clean itself up.

Drawing Points

When I first started programming PC’s back in the 1980s, I learned that the first step to drawing anything on the screen was to draw a simple point. In a Compuserve chat with Andre’ LaMonthe (then a hot shot game developer), he told me if you can draw a pixel, you can draw anything! Take that to heart. It’s the most basic object you can put on the screen. A simple point of light and everything else is simply built up out of many of these points of light. So, that’s our first task is to draw a point. 

Create a file called ex-02.py and in it add the code below:

"""
Prog:   ex-02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw a point 
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *

def main():
    win = GraphWin("Exercise-02, Points", 640, 480)
    # Our first point
    p = Point(320, 200)
    win.getMouse()
    win.close()
    
main()

Now if you run this code you may not see a point. It depends on you’re desktop’s default background color. On my desktop I have a darkula theme and my windows have a dark background. The default color in the graphics library is black and so in my window the point is very difficult to see.

To solve this we need to change the color of the point to something more easily seen. Before we can do that, we need to talk about how computer monitors display color.

Each pixel (picture element) on the screen is actually made up of fields. There is a field for each color channel, Red, Green, and Blue. From these three colors we can make almost any color. You may be saying wait? how can that be? These are not the primary colors I learned about in school. We’ll the colors you were taught were the primary colors are indeed one set of primary colors called the Sink or Subtractive primary colors.  When you add these colors (such as mixing paint or crayons) the color get darker. This is because the colors are reflecting light and adding another color means you’ll be reflecting more light off the surface. However, there is another set of primary colors known as the Source or Additive colors. These colors come from light sources so adding more colors means they get brighter. These are the primary colors used in computer monitors and light emitting devices.For the more curious readers checkout: https://stackoverflow.com/questions/6531536/why-rgb-and-not-ryb and http://en.wikipedia.org/wiki/Additive_color and http://en.wikipedia.org/wiki/Subtractive_color.

The graphics library we’re using has a special function for setting the values of these color channels. The color_rgb() method returns a color object that can be used in many of the library’s drawing methods. We simply need to pass in the values for the r, g, and b color channels. The values for these channels must be integers and be between a value of 0 and 255 inclusive. If we set a channel’s value to 0 it will turn that channel off. For example, if we pass (0,255,127) we are telling the system to add 0 red to the color, full green to the color, and half the available blue to the color. We can generate black by passing (0,0,0) and telling the system to add 0 red, 0 green and 0 blue to the color. We can also generate white by passing (255, 255, 255) to the system. And we can generate a gray scale of 256 levels from black to white by passing equal amounts of each color ex: (68, 68, 68).


"""
Prog:   ex-02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw a point 
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *

def main():
    win = GraphWin("Exercise-02, Points", 640, 480)
    win.setBackground(color_rgb(0,0,0))

    # Our first point
    p = Point(320, 200) 
    p.setFill(color_rgb(255,255,255))
    p.draw(win)
                    
    win.getMouse()
    win.close()
    
main()

Looking at the code we can see we call setBackground() on the window object, passing it a color_rgb object set to produce a black color. This simply sets our windows background to black as you would expect. Next, we create a Point() passing in the x and y coordinates of where it should be drawn. In most computer graphics systems the upper left corner of the screen will be (0,0). The x coordinate grows larger as we travel across the screen from left to right, and the y coordinate grows larger as we travel from the top down. This is a Cartesian coordinate system using only the fourth quadrant and taking the absolute value of the y axis. If that sounds confusing, see: https://www.mathsisfun.com/data/cartesian-coordinates.html or simply google Cartesian Coordinate System.

Next, we see that call the setFill() method on our point and pass in a color_rgb object with three equal values for red, green, and blue. So this will set the point’s color to white. Lastly, we call the draw method on the point so that it is displayed on the screen.

Run the code and you should see a small white dot in the middle of a window with a black background. It’s really that simple.

Now that we can draw a point in the window, let’s have some fun! First, let’s put what we’ve learned about setting colors and plotting points to work. We’ll create a little program to fill our screen with random points of random colors. Enter the code below:


"""
Prog:   ex-02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw a point 
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    width = 640
    height = 480

    win = GraphWin("Exercise-02, Points", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # Each of a random color and at a
    # random position.
    for i in range(10000):
        # Get random position
        x = randint(0, width)
        y = randint(0, height)
        p = Point(x, y)
    
        # Get random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)
        p.setFill(color_rgb(r, g, b))
        p.draw(win)
                    
    win.getMouse()
    win.close()
    

main()

Inspecting the code above the first thing we see is that we’ve imported the random library’s randint() method. We’ll use this method to generate random integer values for our point’s location and it’s color values.

Next, we see we created variables width and height to hold the width and height of the window. We then pass these variables to the GraphWin() method. Then we create a simple loop that iterates from 0 to 9,9999, for a total of 10,000 values. Within this loop we generate a point with random x and y coordinates and then generate a random color and set that color for the point’s color. Then we simply iterate and do it all again until we’ve filled the window with 10,000 random points of random colors.

Drawing Lines With Points

Ok, that was fun. But, we want to be able to draw more than just points. So, let’s try drawing a line. Now, we’ll use one of the most basic line drawing algorithms available. It’s the Bresenham line algorithm. This was the first line drawing algorithm I learned and perhaps the simplest. So it’s a great starting point for us. If you want to learn more about this algorithm check out Wikipedia’s article here: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm.

But first, we'll start by drawing a simple horizontal line as this and the purely vertical lines are the simplest case.

We'll add a horzLine function to our code. To draw a horizontal line all we need to do is draw a series of points starting a (x1,y) and continuing to (x2,y). Notice the y coordinate remains constant for a horizontal line

Create a new file called ex-03.py and add the following code:


"""
Prog:   ex-03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw horizontal
        lines at random positions and colors
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def horzLine(x1, y1, x2, color, win): 
    for x in range(x1, x2):
        p = Point(x, y1)
        p.setFill(color)
        p.draw(win)


def main():

    width = 640
    height = 480

    win = GraphWin("Exercise-03, Lines", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # each with a random color and at a
    # random position/
    for i in range(1000):
        # Get random position
        x1 = randint(0, width)
        x2 = randint(0, width)
        y = randint(0, height)
            
        # Set random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        # Draw our line using generated coordinates and color
        horzLine(x1, y, x2, color_rgb(r, g, b), win)
                      
    win.getMouse()
    win.close()
    
main()

As you can see I’ve added a function to create a horizontal line given the start and end points on the x axis and the y location on the y axis.  All we have to do is walk along the path drawing points until we reach the end of the line. Now it’s your turn. Create a function for drawing a vertical line and call it from the existing loop.

Hopefully, you were able to knock that code out quickly. If not, here’s a hint. All that is needed is to loop over the y coordinate just as we looped over the x coordinate in the horzLine function. Then in the main function’s loop we generate a single x coordinate and a y1 and y2 coordinate for our new line.  Here’s my solution:

"""
Prog:   ex-03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw lines 
        at random positions and colors
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def vertLine(x, y1, y2, color, win):
    for y in range(y1, y2):
        p = Point(x,y)
        p.setFill(color)
        p.draw(win)

def horzLine(x1, y1, x2, color, win):  
    for x in range(x1, x2):
        p = Point(x, y1)
        p.setFill(color)
        p.draw(win)

def main():

    width = 640
    height = 480

    win = GraphWin("Exercise-03, Lines", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # each with a random color and at a
    # random position/
    for i in range(1000):
        # Get random position
        x = randint(0, width)
        y1 = randint(0, height)
        y2 = randint(0, height)
        
            
        # Set random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        # Draw our line using generated coordinates and color
        vertLine(x, y1, y2, color_rgb(r, g, b), win)
                    
    win.getMouse()
    win.close()
    
main()

If you run this code you’ll see random vertical lines drawn at random locations and in random colors.  I think that was pretty straight forward.

Now what about lines that are not horizontal or vertical? How do we draw them? Well we will use a version of Bresenham line algorithm to handle this. This algorithm is designed to handle lines at arbitrary angles.  Therefore it can draw any line in our 2d window. The basic idea of the algorithm is to compute a step and direction such that the point positions fall on integer (x,y) coordinates. This is important as some of the pixels would typically fall partially on two separate (x,y) coordinates. That’s a problem because we can’t draw a partial point. So the algorithm calculates how far we should move in one direction before we take a step in the other direction, so that all our points end up on integer coordinates. For example, how far we should move horizontally before moving vertically. You can find a simple discussion of the algorithm here: https://www.tutorialspoint.com/computer_graphics/line_generation_algorithm.htm and a more in depth discussion at: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm. For those who prefer videos, watch this youtube video for more details: https://www.youtube.com/watch?v=zytBpLlSHms. If you want some truly awesome and detailed text on the subject of computer graphics check out the Graphic Gen series. While they may be older books the information in them is still very relevant.

Ok, so let’s see how we can implement a simple version of this:


"""
Prog:   ex-03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw lines 
        at random positions and colors
        in the window using points.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

# Bresenham's line drawing algorithm
# to handle lines of any orientation
def line(x1, y1, x2, y2, color, win):
    # Calculate dx, sx
    dx = x2 -x1
    if dx < 0:
        sx = -1
    else:
        sx = 1
    # calculate dy, sy
    dy = y2 - y1
    if dy < 0:
        sy = -1
    else: 
        sy = 1
  
    if abs(dx) > abs(dy):
        slope = dy/dx
        pitch = y1 - slope * x1
        while x1 != x2:
            y = slope * x1 + pitch
            p = Point(x1,y)
            p.setFill(color) 
            p.draw(win)
            x1 += sx
    else:
        slope = dx/dy
        pitch = x1 - slope * y1
        while y1 != y2:
            x = slope = slope * y1 + pitch
            p = Point(x,y1) 
            p.setFill(color) 
            p.draw(win)
            y1 += sy  


def vertLine(x, y1, y2, color, win):
    for y in range(y1, y2):
        p = Point(x,y)
        p.setFill(color)
        p.draw(win)


def horzLine(x1, y1, x2, color, win):  
    for x in range(x1, x2):
        p = Point(x, y1)
        p.setFill(color)
        p.draw(win)


def main():

    width = 640
    height = 480
    color = None

    win = GraphWin("Exercise-03, Lines", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # each with a random color and at a
    # random position/
    for i in range(100):
        # Get random position
        x1 = randint(0, width)
        x2 = randint(0, width)
        y1 = randint(0, height)
        y2 = randint(0, height)
            
        # Set random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        # Draw our line using generated coordinates and color
        line(x1, y1, x2, y2, color_rgb(r, g, b), win)
    
    color = color_rgb(255,255,228)

    line(width/2, 0, 0, height/2, color, win)
    line(0, height/2, width/2, height, color, win)
    line(width/2, height, width, height/2, color, win)
    line(width, height/2, width/2, 0, color, win)
                    
    win.getMouse()
    win.close()
    
main()

Now John new that lines would be a desired feature of his library so he included a line drawing function. So why did we draw lines using points then? Well, so you would have an idea of just what the graphics library is doing for you.

Drawing Lines with the Library

Using the Line method of the graphics library is pretty simple. First, you create a Line object and assign it to a variable.

p1 = Point(0,0)
p2 = Point(640,480)
myLine = Line(p1, p2)
myLine.setFill(color_rgb(0,255,255))
myLine.draw(win)

Using line is really no different than using point. There is one aspect we didn’t touch on. That is The width of a line or point. You can set the width of a line or point with:

p1.setWidth(5)
myLine.setWidth(5)

The width value is given in terms of pixels.

Try using the library Line method in our ex-03.py random line program.

Calculating the Distance Between two Points

OK, create a new file called ex-04.py. In this section we are going to calculate the distance between two points. Now if you’ve had geometry in school you may recall that we can calculate this distance with the formula sqrt((x2 – x1)^2 + (y2 – y1)^2). So let’s create a new function called dist() that given two points will return the distance between them.

We don’t really need the graphics library to develop this function however, we can use it to draw the a line between the two points. So I’ve included it in the code in ex-04.py. We’ve developed this function as it is missing (IMHO) from the library (perhaps John left it out so his student had to code it manually?). However, it is easy to develop. enter the code below in to the ex-04.py file and run it. You should she a line from the upper left corner of the screen to the lower right corner. The window size here has been hard coded to (400 x 400). This makes it easy to calculate the length of the line using a calculator so we can confirm our function is working correctly.

#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to find the 
        distance between two points in the 
        window.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from math import *

# Given (x1,y1) and (x2,y2)
# determine the distance 
# between the two points.
def dist(x1, y1, x2, y2):
    return sqrt(((x2-x1)**2) + ((y2 - y1)**2))

def main():
    win = GraphWin("Distance", 400, 400)
    win.setBackground(color_rgb(0,0,0))

    x1 = 0
    y1 = 0

    x2 = 400
    y2 = 400

    p1 = Point(x1, y1)
    p2 = Point(x2, y2)
    myLine = Line(p1, p2)
    myLine.setFill(color_rgb(188, 128, 176))
    myLine.draw(win)

    d = dist(x1, y1, x2, y2)
    
    print("Distance is: " + str(d))

    win.getMouse()
    win.close()

main()

If you run this code, you should see the length print in the terminal window, not the gui. The length should be 565.685…  You can check that with a calculator.

Ok, that brings up another issue. We’ve drawn points and line but what about text? We’ll get to the soon. I promise. But first let’s touch on circles.

Drawing Circles

We’ll begin by plotting our own circles and then touch on the Circle commands in the graphics library. Go ahead and create a new file ex-05.py and leave it empty for now.

Bresenham who we discussed earlier when talking about line drawing actually has an algorithm for circles. There on many circle drawing algorithms each with it’s own advantages and trade offs. We’ll use one of the simpler solutions for circle drawing. We’ll use plot the x,y coordinates of each point along the circumference using sin and cos from the python math library. Note that this is perhaps the poorest performing circle algorithm but it is simple to implement and easy to understand for most middle and high school math students.

The formula for each point on our circles circumference is: x = cos(i)*r + cx; y = sin(i)*r + cy; Where x and y are point coordinates, i is the angle in radians, cx and cy are the circle’s center coordinate, and r is the circle’s radius.  What we want to do is walk through 360 degrees or 2Pi radians. We could convert the degrees to radians but in practice, it works best to simply plot a minimum of 58 points. Why 58? Well 360 degrees / 6.28 ~= 58. So by plotting 58 points we walk all the way around our circle. We’ll see shortly that it’s not quite as easy as this but it’s a start.

Open your ex-05.py file and enter the code show below:


#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw circles 
        using points.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *

# Given the center x, center y
# and radius, draw a circle 
# using points.
def circle(cx, cy, r, color, win):
    # loop over 360 degree arc plotting pixels
    for i in range(0,58):
        x = cos(i)*r + cx;
        y = sin(i)*r + cy
        
        p = Point(x,y)
        p.setFill(color)
        p.draw(win)

def main():
    win = GraphWin("Circles", 400, 400)
    win.setBackground(color_rgb(0,0,0))

    circle(200, 200, 50, color_rgb(255, 128, 200), win)

    win.getMouse()
    win.close()

main()

Now if you run this code you’ll see our circle has many gaps in it’s circumference. This is because our point resolution isn’t high enough for the diameter of the circle we are drawing. So, we have two options:

  1. Shrink the size of our circle until the points meet.
  2. Draw many more points on the circumference of the circle.

Option 1 doesn’t work very well as we may need a larger circle. Option 2 reduces performance but allows us to grow the circle larger without producing gaps. So, we will take this route for now. Simply dump up the number of points produce in the loop from 58 to say 360. Now try the code.

That actually looks pretty good right? Ok, next try increasing the radius from 50 to 100 or 150 in the call to circle(). Oops! With a larger circle our missing points problem comes right back. Using this method to draw circles requires a balance between speed, size, and resolution. We can increase the circle size and resolution at the cost of speed, or reduce the resolution and increase speed at the cost of circle diameter. You’ll find much of engineering, not just Computer Science, requires constant trade-offs. Getting these right for every circumstance is as much an art as mathematics.

Above I only gave you two options for dealing with the gaps in the circles circumference. There are other methods that can be used. One such method is to use line segments to connect the points along the circumference. With this method you would simply place all your points in a list, sort them radially, and then use that list to draw line segments between them. At the end you’ll need to connect the last point to the first with another line segment. This sounds like a great exercise for the reader so I’ll leave it to you.

Drawing Circles with the Circle Method

OK, so you’ve seen that drawing lines and circles isn’t as straight forward as one might think. So let’s see how well the graphics library does on circles. We’ll add a few lines of code and compare out circles.  Edit ex-05.py to read as follows:


#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw circles.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *

# Given the center x, center y
# and radius, draw a circle 
# using points.
def circle(cx, cy, r, color, win):  
    for i in range(0,360):
        x = cos(i)*r + cx;
        y = sin(i)*r + cy

        p = Point(x,y)
        p.setFill(color)
        p.draw(win)

def main():
    win = GraphWin("Circles", 400, 400)
    win.setBackground(color_rgb(0,0,0))

    circle(200, 200, 150, color_rgb(255, 128, 200), win)

    # Draw circle from library
    p1 = Point(200,200)
    c = Circle(p1, 200)
    c.setOutline(color_rgb(220, 128, 164))
    c.draw(win)

    win.getMouse()
    win.close()

main()

Here you can see that using the Circle method from the library follows the same pattern as the Point and Line method. First, we instantiate an object and assign it to a variable, passing coordinates as a Point object. Then we call setOutline to passing a color_rgb object to set the outline color. We could also call setFill and pass a color_rgb object but that would hide the circle we drew ourselves. So if you decide to try setFill with Circle, make the calls to draw the Circle object before calling our own circle function. 

Now if you run this code, you’ll see that the library draws a much nicer circle than we do. This tutorial is more about using and appreciating the graphics library than about all the graphics algorithms. So I’ll leave it to the reader to explore the web in search of better 2D circle drawing algorithms.

Drawing Text in the Window

OK, as promised, we will now cover drawing text. Drawing text is much more complex than circles or lines so we wont try this ourselves. However, I encourage you to give it a try and see what you can accomplish. Playing around with code and ideas will give you a deeper understanding of programming in general, and besides, its a lot of fun!

The Text() method of the graphics.py library instantiates a text object that is centered around it’s anchor point. The anchor point is an (x,y) coordinate you pass to the method to position it in the window. You also need to pass the text string to be displayed.   You can also changed the text on the Text object by calling setText and retreieve the text contained in the object by calling getText. You might think that setting the text color would require a call to setFill or setOutline. But, you would be wrong. The text object requires you call setTextColor and pass a color_rgb object. Other notable text methods include setFace which sets the font family. setAnchor to set the text position (remember the text will be centered around this position. setSize which takes an integer font-point size (not to be confused with Points) between 5 and 36.  setStyle allows you to pass in strings of “bold”, “italic”, “italic bold” and “normal”. These do eactly what you expect.

So let’s see some Text in action. Create a file named ex-06.py and add the code shown below:


#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw text in
        the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint
from math import *

width = 640
height = 480

def main():
    win = GraphWin("Circles", width, height)
    win.setBackground(color_rgb(0,0,0))

    messages = {"John Zelle", "Allen Turning",
                "Tommy Flowers", "Max Newman",
                "Gordon Wlchman", "John Atanasoff",
                "Tony Sale", "William Tutte",
                "Konrad Zuse", "Howard Aiken",
                "Charles Babbage", "Ada Lovelace",
                "Jack Kilby", "Robert Noyce",
                "Grace Hopper", "Ted Hoff",
                "Doug Engelbart", "Paul Otlet"
                "George Stibitz", "Clifford Berry"}
    fonts = ["helvetica","courier", "times roman", "arial"]
    styles = ["normal", "bold", "italic", "bold italic"]

    for msg in messages:
        # randomly pick a display point
        x = randint(100, width-100)
        y = randint(20, height-20)
        # randomly pick a text color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        p = Point(x, y)
        t = Text(p, msg)
        t.setTextColor(color_rgb(r, g, b))

        size = randint(10, 20)
        t.setSize(size)
        # Randomly set the font face and style
        s = randint(0,3)
        f = randint(0, 3)
        style = styles[s]
        print("style: " + style)
        t.setStyle(style)
        font = fonts[f]
        t.setFace(font)

        # draw the text to the window
        t.draw(win)
     
    win.getMouse()
    win.close()

main()

Run the code and you should see a bunch of names randomly placed on the screen at random locations, with random styles, sizes, colors, and font families.  This is just a simple demo. BTW, if these names mean nothing to you I recommend that if you’re serious about computer science you look these people up and read some of their history and any papers they have written.

OK, we can now place text on the screen. But how can we get text from the user? For this, graphics.py contains an object called Entry. Entry objects are little text boxes you enter text into. The Entry object is just an extension of the Text object we just saw. However, it is designed to allow the programmer to retrieve text from the user. Where the Text object is designed to display text to the user. The Entry object has the same set of methods as the Text object including all the font and style methods. It centers itself on the anchor as does the Text object. The only real difference is that the user can type into it.

So, how do we use the Entry to get text from the user? It’s really very simple. Set up the Entry object the same way you do Text and then call the getText() method to read the text out. Note that reading the text is nondestructive. If you need to clear the Entry after reading text you’ll need to call setText() on the object with an empty string. 

I’ve put together a simple example but before you can use it, you need to create a file called button.py and place it in the same folder as your other project code. This file provides a button widget built up out of  Rectangle and Text objects. If you have johns version you can use it. However, modified my version show below has added features. So create a button.py file and place the button code below in it.


"""
Prog:   button.py

Auth:   John Zelle

Mods:   Randall Morgan

Desc:   This is a modified version of John Zelle's
        Button class. Randall Morgan added additional
        features to allow the button outline, background,
        font face, and text style to be change. Also added
        is a method to force a redraw on the button.

Lic:    This code released under GPL to remain 
        compliant with John's original license.

"""

from graphics import *

class Button:

    """A button is a labeled rectangle in a window.
    It is activated or deactivated with the activate()
    and deactivate() methods. The clicked(p) method
    returns true if the button is active and p is inside it."""

    def __init__(self, win, center, width, height, label):
        """ Creates a rectangular button, eg:
        qb = Button(myWin, centerPoint, width, height, 'Quit') """ 

        self.win = win
        w,h = width/2.0, height/2.0
        x,y = center.getX(), center.getY()
        self.xmax, self.xmin = x+w, x-w
        self.ymax, self.ymin = y+h, y-h
        p1 = Point(self.xmin, self.ymin)
        p2 = Point(self.xmax, self.ymax)

        self.text_color = color_rgb(0,0,0)
        self.text_family = "helvetica"
        self.text_style = "bold"
        self.fill = color_rgb(200,200,225) # fill for btn background
        self.border_color = color_rgb(255,255,255)
        
        self.rect = Rectangle(p1,p2)
        self.rect.setOutline(self.border_color)
        self.rect.setFill(self.fill) 
        self.rect.setOutline(self.border_color)
        self.rect.draw(win)

        self.label = Text(center, label)
        self.label.setTextColor(self.text_color)
        self.label.setFace(self.text_family)
        self.label.setStyle(self.text_style)
        self.label.draw(win)
        self.deactivate()
        
    def clicked(self, p):
        "Returns true if button active and p is inside"
        return (self.active and
                self.xmin <= p.getX() <= self.xmax and
                self.ymin <= p.getY() <= self.ymax)

    def getLabel(self):
        "Returns the label string of this button."
        return self.label.getText()

    def activate(self):
        "Sets this button to 'active'."
        self.rect.setFill(self.fill)
        self.rect.setWidth(2)
        self.active = True

    def deactivate(self):
        "Sets this button to 'inactive'."
        self.label.setFill(self.text_color)
        self.rect.setWidth(1)
        self.active = False

    def setFill(self, color):
        "Sets the fill color of the button"
        self.fill = color
        self.rect.setFill(self.fill)
        self.rect.setOutline(self.border_color)

    def setTextColor(self, color):
        self.text_color = color
        self.label.setTextColor(color)

    def setTextFace(family):
        self.text_family = family
        self.label.setTextFace(family)

    def setTextSyle(style):
        self.text_style = style
        self.label.setTextSyle(self.text_style)

    def setBorderColor(self, color):
        self.border_color = color
        self.rect.setOutline(self.border_color)

    def draw(self, win):
        self.rect.undraw()
        self.label.undraw()
        self.rect.draw(self.win)
        self.label.draw(self.win)

I recommend you read through the button code. It’s pretty straight forward and you may find modifications you yourself would like to make.

Time to get on with our text input demo. We will set aside any discussion of the button code and concentrate on the text Entry object’s use.Create a file named ex-07.py and add the code shown next to it. Then run the code.


#!/usr/bin/env python3

"""
Prog:   ex-07.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to get text entered 
        by the user into an Entry object.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from button import *
from random import randint
from math import *

width = 400
height = 400

words = ["Tom Collins", "John Smith", "Alan Turing", "Mike Jones"]

def show_list(offset, words, win):
    y = 40
    for word in words:
        p = Point(offset, y)
        tbox = Text(p, word)
        tbox.setTextColor(color_rgb(255,255,255))
        tbox.draw(win)
        print("show: " + word)
        y += 20

def main():
    global words

    win = GraphWin("Word Sort", width, height)
    win.setBackground(color_rgb(0,0,0))

    p = Point(100, 40)
    box = Entry(p, 20)
    box.setText("Enter a word to sort")
    box.draw(win)

    p1 = Point(100, 80)
                #win, center, width, height, label
    btn = Button(win, p1, 100, 30, "Add to list")
    btn.setFill(color_rgb(200,200,225))
    btn.setBorderColor(color_rgb(255,255,255))
    btn.setTextColor(color_rgb(0,0,0))
    btn.activate()
    btn.draw(win)
   
    show_list(300, words, win)

    while True:
        pos = win.getMouse()
        if btn.clicked(pos):
            # get the text from the 
            # Entry and put it in the list.
            # then redraw the list
            text = box.getText()
            box.setText("")
            words.append(text)
            show_list(300, words, win)

    # Close app    
    win.close()

main()

The ex-07.py app opens a window and displays a Entry fields and a few names in a list on the right. Click in the Entry field and delete the current text. Next, type a name and click the button. The name will be added to the list.

The important thing to realize here is that the call to getText() on the Entry object simply returns immediately with what ever text is in the text field. This means we need to monitor some action, such as clicking a button, to indicate we need to read the text from the Entry object. The infinite while loop handles this for us. Once the loop is entered, the system will remain in the loop until the application is closed. So the win.close() method will never be called. This will cause an error message to be printed on exit. A better approach would have been to add an Exit button so the user could click it to exit cleanly. This would only require another if statement to handle the click on the Exit button. 

Alright, I think this will do for this post. I’ll continue this post soon and add more drawing functions and more information on the library. Their might even be a simple game soon.

Hope you enjoyed the post. If you find it helpful leave a comment and let me know.


Newsletter Powered By : XYZScripts.com