Choose Your Own Pyventure


PurposeEdit

This book is the curriculum book for the Twin Cities ExCo (Experimental College) class Bits and Bites: Programming First Steps

Do you think that programmers are born with keyboards in their hands? Programmers are made, not born -- you too can code with the best of them. If you're interested in breaking down the barriers and mystique around programming, join us! Learn to code in a chill, non-judgmental environment.

Your facilitators, Gregg and Amanda, come from non-traditional programming backgrounds, and used to be N00bs. We have no patience for alpha geeks, macho baloney, and geek superiority.

Our big project is a web application that allows you to play a "Choose Your Own Adventure" that you write yourself! (example: http://cyoa.lind-beil.net/).

All instruction is done in the Python language, a free, open-source, cross-platform, powerful yet easy to learn language. We'll help get you going, introducing new concepts weekly. There will be hands-on assignments, lots of time for questions, and a loosely structured feel. Hacking is about liberation and democratizing power.

Prerequisites: Access to a computer where you can run or install programs. Online only is fine, but learning is better in meat space (where you'll need access to a laptop, or really strong arms to haul your desktop).

Programmers with experience are also welcome as learners or mentors.

Please let us know about any requirements around mobility, neurodiversity, or child-care needs, and we will do our best to meet them.



Why Another Python Book?Edit

We were inspired by:

Kirrily Roberts' OSCON Presentation and Dreamwidth's Python vs. Ruby Deathmatch.

About the AuthorsEdit

The AudienceEdit

Installing PythonEdit

WindowsEdit

1a. Python, from the Python website

You will want the newest 2.x series (probably 2.6.x) release, NOT a 3.x.x release.[1] Python 3 has some differences (primarily around strings) that this class doesn't address.

If you're more adventurous, feel free to try one of the installations towards the bottom of the page:

  • ActiveState ActivePython (not open source)
  • Enthought Python Distribution (a commercial distribution for scientific computing)
  • Portable Python (Python and add-on packages configured to run off a portable dice) (Recommended if you can't install python system-wide, and need to run it off a USB stick, SD card, or the like)

Those distributions have additional modules (bundles of code) we're not going to use. If you get serious with Python, installing one of these bundles can be much easier than installing pieces piecemeal. Install it using the usual windows methods.

1b. Test your Python installation.

   start > run > cmd  [OK]
  

This will open a Windows cmd window. In it, type python:

   C:\Documents and Settings\Gregg>python
   Python 2.4.3 - [some other info, perhaps]
   >>> 'hello'
   'hello'

If you see something like this, then you're good! If (sadpants), you see something like

   'python' is not recognized as an
   internal or external command, operable
   program or batch file.

then python (the 'interpreter' program that can translate Python into "computer instructions") isn't on your path (see Putting Python in Your Path below). Then try calling it like this (assuming Python2.6, installed in the usual location, C:\Python26):

  \> C:\Python26\python.exe


2a. Install A Text Editor.

Word processors, including Microsoft Word and friends, are terrible for writing code, since they conflate layout formatting and text.Simpler is better. That said, Notepad is terrible also, because it automagically [2] appends on '.txt' onto filenames, and other niceties.

Key features of a good programming editor:

  • syntax highlighting
  • fixed-width font
  • multiple tabbed interface.

A good, free (as in beer, and open-source) one that we like a lot is SciTE [1]. This program also has a "no install" version found at from SourceForge The portable versions doesn't quite have all the features of the installed version, but is quite capable. A good sign in programs is when they can exist in a form that doesn't require an installer. This implies the developers don't want to interfere with a running system, or damage anything, and make it easy to get rid of the program if you don't like it.


2b. Test your installation

Double click on SciTE, or choose it from the Programs menu, or click on the executable, in the usual Windows ways. You should get a Notepad looking workspace.

Copy and paste this in:


 # here is some python code
 1 # an int
 b = 'some string'  # string
 if 2 > 1:  print "sure looks bigger"

Then, in the menu: Language > Python. The code should change change colors, and the various parts (variables, integers, strings, comments) will be nice colors.

Mac OS XEdit

Good news Mac users! Python comes preinstalled as part of the Mac OS. Check it out:

  1. In Finder, navigate to Applications -> Utilities -> Terminal and start up the Terminal
  2. Type python and hit enter
  3. You will see something like this:
 Python 2.6.2 (r262:71600, Apr 16 2009, 09:17:39) 
 GCC 4.0.1 (Apple Computer, Inc. build 5250)] on darwin
 Type "help", "copyright", "credits" or "license" for more information. (
 >>> print "hello"
 'hello'


You can do a lot in the Python command prompt but writing more complex problems will be easier with a text editor. TextWrangler is a good text editor for Mac users. Download it from the Barebones Software Site

BothEdit

(Optional) Install ipython

Ipython is a python package that gives a much nicer command-line environment, which includes syntax highlighting, history, and a variety of debugging tools and improvements. Download it from the Ipython site.


Putting Python In Your PathEdit

In order to run programs, your operating system looks in various places, and tries to match the name of the program / command you typed with some programs along the way. This list of folders and locations is called the (System) Path. Confusingly, path also refers to the path to a particular file or directory on a system, described as a 'full path' or 'relative path', depending on context. This article is about how to put the Python interpreter on the System Path to make our command line know what to do when the user types python.

WindowsEdit

First see if the directory is on the path or not. From a windows cmd prompt:

   > path

This will output the system path. See if C:\Python26; (or equivalent) is on it. If not, you need to add it.

   control panel > system >  advanced > |Environmental Variables| > system variables -> Path

This needs to include: C:\Python26; (or equivalent). If you put it at the front, it will be the first place looked. You can also add it at the end, which is possibly saner.

Then restart your prompt, and try typing 'python'. If it all worked, you should get the soon-to-be-familiar ">>>" prompt.


Python IDEsEdit

In this course / book, we won't use any IDE's (Integrated Development Environments), because this is a course about programming, not learning an IDE. For the advanced programmer, they can speed up work by helping organize code, autocompleting variable names, and other things. Gregg doesn't use or endorse any of these, and tends to use Scite and Git for his dev work.

PyScripter is a free IDE (available for Windows. If you have previous programming experience - this is similar to the Borland Delphi IDE. You can download PyScripter from PyScipter project site.

There are other IDEs with Python code support as well (Komodo, Eclipse).

LessonsEdit

Lesson 0: First Bite of PythonEdit

InteractiveEdit

First taste

print "Hello, world!"

After running this, you should see:

 Hello, world!


Play Around With Turtle

The Python turtle module, is a simple reimplementation of a LOGO-like language.

From your python prompt:

  1. # import everything from the turtle module
  2. # import:  make them available for use
  3. # everything:  (mostly) everything (there are some exceptions)
  4. # the turtle module:  a collection of functions (actions), constants,
  5. #    and other useful stuff that is grouped into one 'space' (a namespace)
  6. #    named turtle, for easy of memory, and general good sense
  7. >>> from turtle import *  
  8. >>> circle(80)   # this will draw a circle with a diameter of 80
  9. >>> reset()  # reset the screen
  10. >>> forward(10)  # make the turtle go forward 10

All the commands are listed at the Turtle reference documentation

Batch / From A FileEdit

Save this in your favorite text editor, as 'lesson0.py'

print "Look, Ma, Non-interactive!"

Then open a prompt, navigate (see below) to where you saved the file and run:

   prompt> python lesson0.py


NavigationEdit

cd changes directories. cd .. goes up a directory

ls|dir lists contents of a directory. By default, it lists the current directory. 'ls' is bash/mac; 'dir' is windows'.

pwd print working directory. Where the heck am I? (This is spelled 'cd' (no arguments) in Windows).

Lesson 1: Welcome to Mysterious MansionEdit

no line numbers, but can be copy and pasted

## anything from '#' is a comment, and gets ignored.  
## all my editorial comments will start with '##' -- GRL
 
## some text describing what this is
# a simple choose your own adventure
 
## 'print' prints to the screen.  
print "Welcome to MYSTERIOUS MANSION."
 
print "You are at a mysterious door.  The door is clearly marked -- 'Open Me And Die!'."  
 
## in python, strings can be single or double-quoted
print 'Do you want to open the door?'
 
## raw_input gets input from the user
## Here, we take the input, and *assign* it to a variable called 'ans'
ans = raw_input("please type 'yes' or 'no' ")
 
## conditionals
## see if the user's answer is interesting or not
if ans=="yes":
    print "That was foolish!  You are now dead."
## elif means "else-if"
elif ans == "no":
    print "That was wise!  You are alive, but thoroughly bored."
## else is a 'catch-all' for "any condition not all ready covered"
else:
    print "I don't know what to do, based on what you said, which was, |", ans, "|"
 
print "Thank you for playing!"


  1. ## anything from '#' is a comment, and gets ignored.  
  2. ## all my editorial comments will start with '##' -- GRL
  3.  
  4. ## some text describng what this is
  5. # a simple choose your own adventure
  6.  
  7. ## 'print' prints to the screen.  
  8. print "Welcome to MYSTERIOUS MANSION."
  9.  
  10. print "You are at a mysterious door.  The door is clearly marked -- 'Open Me And Die!'."  
  11.  
  12. ## in python, strings can be single or double-quoted
  13. print 'Do you want to open the door?'
  14.  
  15. ## raw_input gets input from the user
  16. ## Here, we take the input, and *assign* it to a variable called 'ans'
  17. ans = raw_input("please type 'yes' or 'no' ")
  18.  
  19. ## conditionals
  20. ## see if the user's answer is interesting or not
  21. if ans=="yes":
  22.     print "That was foolish!  You are now dead."
  23. ## elif means "else-if"
  24. elif ans == "no":
  25.     print "That was wise!  You are alive, but thoroughly bored."
  26. ## else is a 'catch-all' for "any condition not all ready covered"
  27. else:
  28.     print "I don't know what to do, based on what you said, which was, |", ans, "|"
  29.  
  30. print "Thank you for playing!"

HomeworkEdit

Lesson 2: Mapping The MazeEdit

GoalsEdit

  1. Nested conditional statements
  2. Introduce new python data type, the dict
  3. Introduction to functions

Nested Conditional StatementsEdit

How would we expand our code to encompass more rooms and paths? We can use nested conditional statements:

## revised mysterious house, using nested conditionals.
 
print "Welcome to Mysterious House!\n\n"
name = raw_input("What is your name? ").strip().title()
 
print '''You are in the *foyer*.  There is a mirror on the wall.  In the mirror,
it says in blood (or possibly ketchup, if you're squeamish\n\n''' + \
  name[::-1].upper() + '''
 
creepy.   Very creepy.  And MYSTERIOUS!
 
There is a door'''
ans = raw_input('go (through the door) or stay? ')
if ans == 'go':
    print '''you are in a dark hallway.  It's creepy, but there is \
    a delicious smell from down the hall.  You go towards it.  
 
    The lit room at the end of the hall is a kitchen.   You're ravenous.
    There is a cake on the table. 
    '''
    ans = raw_input("eat the cake (yes or no)? ")
    if ans == "eat" or ans == "yes":  
        print "mmmm.... delicious cake"
        ans = raw_input( '''You feel guilty.  Choose a reason:  
 
a.  it's rude to eat someone else's cake
b.  you ate earlier, and were still pretty full
c.  you're allergic to cake\n\n''')
        if ans=='a':
            print "You're right, it is rude"
        elif ans=='b':
            print "Well, it's not like there is tupperware around to take it for later"
        else:
            ans = raw_input( "Oh no!  What kind of allergy?  [gluten or anaphalectic]? " )
            if ans[0] == 'g':
                print '''THE ORACLE PREDICTS.... soon you will need to find a Mysterious........... 
 
 
         bathroom.
'''
    else:  # no cake
        print '''No cake?  REALLY!  Instead you drink beer, pass out, and \
            are eaten by a grue'''
 
else:   # no door
    ans = raw_input('yes or no? ')
    if ans == 'yes':
        print '''I see you are a person of action!  Too bad you're hanging about in \
a foyer!'''
    else:
        print '''I sometimes get that way in the winter too'''
 
print "\n\nThank you for playing,", name


Exercises:

  1. What are benefits and drawbacks to this approach?
  2. Suppose that you wanted to give the user a second chance. If they choose to 'stay', then 'yes', send them through the door. How would you implement this?
  3. How would this approach scale to hundreds of rooms?
  4. Moving towards maps
    1. rewrite the code, numbering the rooms / state
    2. make a map (flowchart) of all the possible paths through the game


Python DictionariesEdit

0-level: is like a paper dictionary, in that there are entries and definitions. Like in a paper dictionary, this gives particular definitions' names for ease of finding. It is much easier to go look up the definition for "octothorp" than have to remember that the definition for "octothorp" is on page 861. In Python, we call the entries keys and the definitions values. dicts are used for lots of tasks in python, including indexing, graphs, and data storage.

1-level: in Python there are many ways to construct a dictionary. Here is one:

    symbol_names = { '#':  'octothorp', '!': 'exclamation point', '?': 'question mark',
          ' ': 'space', ',': 'comma', '.': 'full stop', ':': 'colon' }

We could use this to print out the letters punction in a sentence like this:

   for letter in "Here is my sentence.  It has grawlix:  #!?!":
       # there is shorthand for the next for line:  dict.get(thing, default)
       # print symbol_names.get(letter,letter)
       if letter in symbol_names:  
           print symbol_names[letter]  # [] 'indexes' the dict
                         # some_dict[key] -> get value for key in some_dict
                         # by analogue, some_dict[key] = value sets it.
       else:
           print letter    

2-level: Python dictionaries (dicts) aren't 'really' like paper dictionaries.

a. dict's have no inherent order, and especially don't have alphabetical order. In memory, the thing after 'octothorp' might be '2'. Think of them more as someone's kitchen. There well labeled drawers all over. When you ask for something (like a mixing spoon), the kitchen owner says, "those are in Drawer 13, let me get one". It doesn't matter what else is in Drawer 13.

b. The keys don't have to be strings, and values can be almost anything, including strings, lists, objects, functions and other dicts. The keys do have to be immuatable though.

3-level: In other languages dicts are called (variously) hashes, associative arrays, or maps (mappings). Map(ping) emphasizes the 'correspondence' aspect. Associative array is obvious (associate an identifier with a position in an array), but why 'hash'? Well, as it turns out, it's not just a love for breakfast meat, or Amsterdam. Among other things, a 'hash' is a function that takes data and returns a string, in a systematic way. As an example, the hash function first_char(str) -> str is like the hash that paperdictionaries use. The problem with it is that then some sections of thedictionary are big (like s and t) while some (e.g., q, x, z) are very small.

In computing terms, it's much better if the hash is uniform, meaning that the output is spread evenly over the answer space. Thinking back to our kitchen example earlier, uniform hashing is important so that no particular drawer gets too full. Dicts have fast lookup because they make lots of shallow drawers. If it's fast to figure out which drawer you need to look in, and if there's not much in each drawer, then finding particular items is easy. In terms of Computational Complexity, lookup and entry are O(1).


Exercises:

'Put it all together'

Turn the game flowmap into a dictionary of lists, like this:

   gamemap = {1: [2,3], 2: [4,5]}

HomeworkEdit

a. make it: make a dict of the digit names, then a function that prints the name of each digit in an inputted string.

b. make it robust: make it so that it can handle inputted ints, and strip out all non-digits, so that output like 1111 and '2 and 1 is 3' will be handled well

Lesson 3: Building a Better ParserEdit

GoalsEdit

  1. Functions
    • signatures
    • arguments
      1. named and positional
      2. default
    • Printing is not Returning
    • docstrings
  2. Recursive Functions
    • (See immediate previous)
  3. Imports
  4. Randomness (PRNGs)
  5. Data Types
    • bool Boolean (True/False)
    • list Lists
    • None None

Anatomy of a FunctionEdit

Functions and data structures are the twin bases of programming. At its core programming is about taking state, and changing it. As such, we're going to take a little time here to talk more heavily about functions, in a level of syntactic detail that we've avoided until now.

In Python, function definitions are spelled:

  • def :: signals to python that we're defining a function
  • function_name :: a valid identifier[3] naming the function
  • ([optional named and positional arguments])
    • the parentheses are required, but the function may or may not take arguments, which are execution-time parameters for the function, that can affect how it runs.
    • python supports named and positional arguments, as well as optional values[4], which we'll explore more below.
  • ':' :: the colon character
  • one or more lines of code. If you want a function to do nothing, use the pass statement
  • all functions return a value. By default this is the special value None


Function SignaturesEdit

A function signature simply describes the inputs and outputs of a function in a conventient shorthand, using pseudocode[5]. As an example, look at range from the standard library:

   range([start,] stop[, step]) -> list of integers

The bracketed arguments imply that these are optional arguments. Thus if one argument is given, it will go in the 'stop' slot, if two, then 'start','stop', and if three are given 'start','stop','step'.


Creating Your Own FunctionsEdit

We've come across a Python function already: raw_input(prompt) -> string. This is an example of one of the many "built in" functions that are part of the Python language standard distribution (thus, always available). Other functions live in modules that need to be imported. Some modules come with Python (the standard library), or you can create your own, or use ones downloaded from the internet[6]. Using built-in functions is very easy and creating your own is not much harder!


Functions Case Study: Wandering Grue

Let's say you'd like to have a wandering Grue in your Mysterious House that pops up in a room (or rooms) randomly.

One way to do this is to make a function that decides whether the Grue is in the room or not:

Test Frameowrk

Later in our complete code, we will see code similar to:

eaten=grue()
if eaten:
    print "Sadly, you were torn limb-from-limb by the Grue and suffered a slow, painful death."
else:
    print "Congratulations, you have not been eaten by the Grue! May you have a long happy life."

Here we introduce a new python data type: boolean. Python booleans can take two values True and False. These code fragments are equivalent:

   if x > 3:
       print "yep!"
   
   if x > 3 is True:
       print "yep!"
   
   if bool(x>3) is True:
       print "yep!"

The "if" syntax implies if predicate is True. In Python, most things evaluate to True, except: None, False, 0 (0.0, etc.), empty strings, zero-length lists, dicts, and few other oddities [7].


Variation 1: not very random

def grue_always():
    ''' this grue always appears.  returns true'''
    return True

Our not very random grue always appears.

Exercise: Make the reverse -- a grue that never appears. The signature of the function should be grue_never() -> False


Variation 2: the 50/50 Grue

import random 
## random is a Python module, as mentioned above. 
## We need to import it to access the random() function.
## now to begin the function definition
## Everything inside the function definition is indented! Remember, white space matters!
def random_grue(): 
    ''' boolean.  a grue that appears 50% of the time '''
    ## we want something that will return True 50% of the time.
    ## one method:  get a random float between (0,1), and return True if it's over .5
    ## now we need a random number. random() will give us one between 0 and 1
    n=random.random() ## the random before the dot tells Python what module to look in for the function, which is the one we imported above
    if n > 0.5:
        grue=1 ## 1 == True
    else:
        grue=0 ## 0 == False
 
    return grue ## returning allows us to capture the value


So what does the random_grue() function do? Let's try it. In the Python interpreter:

    >>> import random
    >>> def random_grue():
            n=random.random()
            if n>0/5:
                    grue = 1
            else:
                    grue = 0
            return grue
  
    >>> random_grue()
    1

The first command is to import the random module, the second is to define the function and the third is to actually call the function. The 1 is the return of the function. You may get a 1 or a 0 depending on the number random() generated. (Hint: try running it several times)

> A digression on pseudorandom numbers, and tips for getting the same ones every time!


Variation 2: the Moody Grue

import random 
def grue_moody(cutoff=.5): 
    ''' boolean.  a grue that appears (100*cutoff)% of the time '''
    n=random.random() 
    above_cutoff = n < cutoff
    return above_cutoff
 
def grue_moody2(cutoff=.5): 
    ''' boolean.  a grue that appears (100*cutoff)% of the time '''
    return random.random() < cutoff

Note that we simplified down the function quite a bit by returning the boolean value directly (especially in 'grue_moody2'), rather than doing any conditional logic to get a 1 or 0. Also notices that we specified a default value for the argument cutoff.

Exercises

  1. Predict the behaviour of these functions calls. Then try them.
    • grue_moody()
    • grue_moody(-1)
    • grue_moody(1)
    • grue_moody("a")
    • grue_moody([1,2,3])
  2. Fix the code so that it prints an angry message and returns None if n is outside the interval (0,1).
  3. try help(grue_moody). What do you see?
  4. what are the types of random, random.random, random.random()


Variation 3: the location, location, location Grue

In our final variation, we want a grue that:

  • has different percentages of appearing based on which page
  • should have zero chance of appearing in the main room "foyer"
  • should have a default chance of appearing of 5% in rooms that aren't otherwise described
import random 
grue_fractions = { 'foyer':0, 'thedark': 1.0, 'nocake': .2 }
 
def location_grue(room=None, base=.05, cutoffs=dict()): 
    ''' (boolean), does a grue appear in the room?
    room : str room name
    base : 'cutoff', float between (0,1), for base (room not found)
    cutoffs: dict of room_name: cutoff (float between 0,1)
    '''
    cutoff = cutoffs[room]
    return random.random() < cutoff


Exercises

  1. as written, location_grue has some bugs, and doesn't meet spec. Identify and fix them.
    1. try: location_grue('foyer', cutoffs=grue_fractions)
    2. what happens if 'room' isn't in 'cutoffs'?
    3. research the 'get' method of dictionarys... help({}.get). Use this method to fix the code.

import this -- More on ModulesEdit

A module is any file containing Python definitions and statements that we can access from other python programs. To import a module, it must either be in the standard library[8], on the PYTHONPATH, or a file in the same directory as the running python process. Let's explore a the random module, which we'll use for our wandering Grue!

   # random is in the standard library
   >>> import random
   >>> dir(random)
   ['BPF', 'LOG4', 'NV_MAGICCONST', 'Random', 'SG_MAGICCONST', 'TWOPI', 'WichmannHill', '_BuiltinMethodType', '__all__',
   '__builtins__', '__doc__', '__file__', '__name__', '_acos', '_cos', '_e', '_exp', '_floor', '_inst', '_log', '_pi', '_random',
   '_sin', '_sqrt', '_test', '_test_generator', 'betavariate', 'choice', 'cunifvariate', 'expovariate', 'gammavariate', 'gauss',
   'getstate', 'jumpahead', 'lognormvariate', 'normalvariate', 'paretovariate', 'randint', 'random', 'randrange', 'sample', 'seed',
   'setstate', 'shuffle', 'stdgamma', 'uniform', 'vonmisesvariate', 'weibullvariate']

You can also type >>> help(random), or from the command line (not the Python interpreter)

   $ pydoc random
   $ pydoc random.shuffle # for example

to see full docstring based help files.

Notice the function random() appears in that list. To call a function from a module, you need to tell Python what module you're using as we did in the above function with the Grue.

   >>> import random
   >>> random.random()
   0.73015823962912774
   >>> random.randint(2,800)
   158

Check out the Python documentation for the module you want to use if you want to know what each function does and how to use it. Here is the documentation for the random module: http://docs.python.org/library/random.html

Wandering_Grue

Exercises

  1. Explore import more. In some directory, copy this text to a file, called my.py
    _sekrit = True
    var = 1
    def f():  return "yep!"
    

    Open a python prompt in that directory. Try running this code:

    import my
    import my as also_my
     
    my.var is also_my.var
    print my.var
    print also_my.f()
    print my._sekrit
    print _sekrit
    print var
    print f()
     
    from my import *
    print _sekrit
    print var
    print f()
    

    Q: What is going on with _sekrit? What does import * from my really do?


Recursive FunctionsEdit

See Recursive Functions

 You loop, until it's time not to loop.  -- adpated from Patrick Swayze (RIP), Roadhouse


Recursive functions are functions that call themselves from inside their definition. Confusing! Be forewarned: recursion is kind of spicy stuff, so don't worry if it takes you awhile to wrap your brain around it. Here's an example of a recursive function:

def yesorno():
     ans=raw_input("Yes or No? ").lower()
     if ans=='yes':
             print "Aren't you a yes man!"
     elif ans=='no':
             print "Why are you so disagreeable?!"
     else:
             print "You said:", ans, ". Answer the question!!!"
             yesorno()  # recursive call, 'R'

Notice that at the end, we call the function yesorno() at point 'R' from inside the function definition. How does Python know what to do with a function if it hasn't been defined yet?!

The answer is: it doesn't. And it doesn't need to. There are multiple stages in interpreting code. During definition (when you define a function), Python looks over the code for any syntax errors, like improper indentation, or you type 'pring' instead of 'print', but it doesn't actually execute any of the code. It sees def yesorno():, and assigns the token "yesorno" as a reference to a function (defined by the definition). yesorno is just like any other variable. Since it's syntactically correct, it continues through the code. It's only when you call the function (execution) that Python cares about the fact that it's a function and will execute it as thus, since it's already defined!

Before continuing, try the code. Give it some bad answers.

The point of recursion here is so that the question "Yes or No? " will keep getting asked until the user inputs either 'yes' or 'no'. This prompt is much better behaved (and robust) than our original conditional/branching logic in Lesson 1.

Here is a more complicated example of how we can use recursive function in our Choose Your Own Adventure games:

def move(choices):
    ## choices should be a list in our dictionary of pages, here named "book"
    for x in choices:
        print x	## this displays the choices to the player
    print "" ## print a blank line, for looks
    text = "What next? "
    ans= raw_input(text)
    ans.lower().strip()
    if ans in choices: ## check through the list "choices" to see if the input is valid
	return ans ## return choice
    else:
        print "That answer,", ans, ", isn't in the choices"
        return move(choices) ## keep calling the function until the user inputs a valid answer

There are two things to notice here - the function accepts an argument, "choices," and when the function is called at the end of the definition it is called with the argument choices. In the Python interpreter, define your dictionary called book like this (feel free to be creative and change the pages around, etc.):

   >>>book={
       'foyer' : {'desc' : "Some text", 'choices' : ['bathroom','kitchen',], },
       'bathroom' : {'desc':"Some text" , 'choices' : ['change toilet paper','leave'] },
       'kitchen' : {'desc':"Some text", 'choices' : ['eat the cake', "don't eat the cake"]},
       'eat the cake':{'desc':"Some text",'choices':['have a glass of water','wash the plate']},
       "don't eat the cake":{'desc':"Some text",'choices':['put the cake in the fridge','sit down']},
       }

Then define the function as you've done in the examples above. Call the function like this:

   >>> move(book['foyer']['choices'])

What happens should look like this:

   >>> move(book['foyer']['choices'])
   bathroom
   kitchen
   What next? 


Type in something other than the choices listed. What happens? Type in one of the listed choices. What happens then?

Now try defining the function like this:

def move(choices):
    for x in choices:
        print x
    print ""
    text = "What next? "
    ans= raw_input(text)
    ans.lower().strip()
    if ans in choices: 
	return ans 
    else:
        print "That answer,", ans, ", isn't in the choices"
        return move()

Call the function as before. Try giving it a choice that isn't listed. What happens? You should get an error like this:

   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "<stdin>", line 12, in move
   TypeError: move() takes exactly 1 argument (0 given)

The problem is explained in the last line of the error. The function we defined takes exactly 1 argument, but when we called it the second time (by giving it the invalid choice) we didn't give it any arguments so it flubs.

A Bulletproof (Okay, Bullet-Resistant) ParserEdit

HomeworkEdit

Lesson 4: Putting It All TogetherEdit

GoalsEdit

  1. midclass review
  2. CYOA data and action model
  3. using the command line to call a script

Midclass ReviewEdit

FIRST!!!, do the Midclass Review

Modeling A Choose Your Own Adventure GameEdit

Computer programs are composed of DATA modeling a problem space and FUNCTIONS to operate on (manipulate, transform, and display) those data.

Data, or What is the What?Edit

Imagine that you are holding a choose your adventure book.

  • What is it? What are its parts?
    Answer

    It is a paper BOOK composed of PAGES. It has a FRONT COVER and BACK COVER. These PAGES contain text, parts of the story, or can be special pages like copyright pages, cataloging information, etc.

  • How do you know where to start?
    Answer

    By convention, pages start at the front, in English-language books

    • Does Page 1 mean anything?
      Answer

      Nope, it's just convention.

  • What are the parts of each page?
  • Answer

    pages contain a PAGE NUMBER, some TEXT and CHOICES


    Let's use our real-world domain-space knowledge to create a practical data model. Our data model should be sophisticated enough to model the domain, without getting over-burdened in details. If we need to make it more detailed later, we can.

    For each of these pieces, decide the following:

    • how many of them are there? Zero, zero-or-more (0+), (exactly) one, one or more (1+), or some other exact number?
    • how does one identify them? By unique id (U), index position, or in some other way?
    • is the item contained in / part of another structure? Does it have children or parents?
    1. Book
    2. Page
    3. Page Number
    4. Description
    5. Choice


    Answer
    • Book: one(exactly), contains pages
    • Page: 0+, unique ids, part of book, contains description, etc.
    • Page Number: int (in CYOA books), part of a page, referenced by choices. Must be unique.
    • Description: 1, part of a specific page, composed of text
    • Choice: 0+, belonging to a page. In paper CYOA's, these are identified by index (1st choice, 2nd choice, etc.). Each contains some text and the id of the next page to go to.


    Data DiagramEdit

    Putting this all together, a model for a Choose Your Own Adventure might be:

       DATA MODEL FOR A CYOA, a kind of decision tree 
       
       Book CONTAINING
          pages CONTAINING  (1+)
              unique id (1)
              description (1)
                  words in paragraphs (0+)
              choices (0+ -> leaf/ending, 1+ -> node)
                  choice text / description / information 
                  jump / pointer to another unique id
    


    Next, we map these ideas on Python data types:

       Book = dict() with key:value -> pageid:Page
           Page = dict() with key:value
              pageid: str 
              desc: str
              choices: iterable/sequence of choices (list, dict, or set)
                  choice contains:
                       choicetext # str, something like "Go through the door"
                       pageid     # pointer to another page
    

    Here is an example data structure for our game.

    TheCaveOfTime = { 
       1:  dict(
              desc='you enter the cave of time',
              choices = [
                  ('go into the cave', 81),
                  ('fall asleep', 'the dark'),
              ]
            ),
       81:  dict(
             desc='Wow, a cave of wonders!',
             choices = [],
            ),
       'the dark':  dict(
            desc="You're eaten by a grue",
            choices = []
            ),
    }
    


    For the sake of simplicity, we have ignored some bits (copyright, front and back cover, etc.). But it's easy to add them back in if one is so inclined.

    Exercises:

    • Note we have a mix of strings and ints naming our pages. What are the problems with this approach? Benefits? On the whole, is this wise, and if not, what is a better approach?
    • Will allowing tokens like "the dark" as room names cause us problems?

    Extra credit:

    • Recall that our 'game data' is simply a dictionary, and recall that it is trivial to add new items to a dictionary. Use this to include COPYRIGHT, INTRODUCTION, and AUTHOR INFORMATION to our game.
      Answer

      One idea is to have 'special' pages, that we define by convention to be 'AUTHOR', 'COPYRIGHT', and 'INTRODUCTION', like this:

      TheCaveOfTime = { 
          'AUTHOR':  'Jane Q. Fancypants',
          'COPYRIGHT':  'copyright 2009, Creative Commons License',
          'INTRODUCTION':  'It was a long and tedious evening...',
      }
      

      Of course, in this schema, we have to make sure not to have any choices point to these keys, or havoc will ensue. A Python mantra is to assume we're all adults. Now we could have choices that point to "AUTHOR", but we have to trust that we'll be wise, and not do so. Nothing in the language, or the data structure actually enforces this. In this example, it would be wise to have a (human) rule like, "ALLCAPS indicates that this this is not a real pageid, but a special bit of data, so don't have any choices point here".

    ActionsEdit
    • Imagine yourself holding the book. What actions does one do to interact with the story?
    Answer

    Your answers may include:

    • start the book at the entry point (page 1)
    • read the page
    • see choices for where to go next
    • choose from the choices
    • branch / move
    • How does the book end? How does one know when to stop reading?
    Answer

    There are no choices from a page. The book may also have a message like, THE END, to indicate the ending.


    Putting It All TogetherEdit

    Let's model the game flow using pseudocode. This pseudocode will help us figure out what functions we need to create, and what they should take as arguments. There are an infinite number of ways to do any programming task, and we will indicate, and the provisional nature of our pseudocode throughout. We may even through it all out and start from scratch if we need to!

       GAME FLOW
       
       display_description(roomid)  # print to screen
           print Book[roomid]['desc']
           print choices (number them?)
       
       next_room_id = user_choose( choices? )
       
       [repeat until user QUITS or we reach an ENDING! 
       (An ending means that the user has no place to go)
    


    Some Code VariationsEdit

    First Draft - 20 Miles of Bad CodeEdit

    Check out this code here . Warning! There be some bad code behind that link. Think about the major things that are wrong with it and how it can be fixed and improved.

    Second Draft - Patching the HolesEdit

    Second draft

    Exercises

    1. Note that move() is now while-based, rather than recursive.
    2. Write a 'pages' dict that will fail check_pages
    3. ADVANCED. Using your favorite search engine, investigate the "if '__name__' == 'main'" stuff at the end of the file.
      Hints
      • start interactive python. Then: print __name__
      • [__main__]
      • it has to do with command-line usage vs import


    main and calling from the command lineEdit

    (if name is main, import context and more) -- TODO

    Lesson 5: World Wide WobblyEdit

    GoalsEdit

    1. Understanding the HTTP Request
    2. Introducing web.py

    Snowpants, a Love StoryEdit

    So, you think you know the web. Sure, you can surf the web, send twits, read electronic telegrams, and control a Level 50 Dermatologist in World of Wartcraft, but do you know what goes on under the hood?

    Dramatis Personnae (the players)

    • Minnie, a web user in Minneapolis
    • Brow, a web browser
    • Servo, a web server program
    • Mac, a computer in Yellowknife
    • YellowSnow, a NSP (network service provider) in Yellowknife

    The curtain opens. Minnie types http://sendwarmclothes.org/snowpants/send/ into her web browser. After filling out a form, she receives an email and a text message saying "Fear not! Longjohns are on the way!" and weeks later, they arrive. The crowd cheers, and legs are warm!

    So, what happens backstage?

    1. When Minnie types the url into Brow, and hits enter, some magic happens and the domain name http://sendwarmclothes.org/ is translated (resolved) into an IP address. The browser program creates an HTTP Request, which is just a specially formatted text message, which it tries to send. Inside this message is a lot of information... where the request comes from, the URL host and path requested, timestamps and other housekeeping information, and more.
    2. More routing magic happens happens, and the request makes its way over wires and by dogsled to Yellowknife, where Mac, the computer that hosts the "sendwarmclothes.org" website, lives.
    3. Mac has lots of programs that run on it. It can handle email, has a popular Boggle program that users love, and produces enough heat that the employees at YellowSnow huddle around it for warmth. The staff of "Send Warm Clothes" actually live in San Diego, where winter clothes are extermely cheap, but decided they would have more snow-cred if their website was hosted in western Canada. Along with this busy and fulfilling life, Mac runs a program called Servo, a web server program. Some web servers include Apache and IIS, but there are many others.
    4. Servo's job in life is to listen on a certain set of ports (among these: 80 for HTTP and 443 for HTTPS), and respond if any messages (HTTP requests) come in on those ports.
      • 0-level: In concept, responding is quite simple. When an HTTP request comes in, the server knows the format of these special messages, decodes them, and returns a specially formatted text message (the response) in reply.
      • 1-level: The magic happens by putting special things in the body of the response. As part of the response, it describes what kind of special text it's returning, such as: text/html, video/quicktime, or application/msword. It's the job of the browser to figure what to do with this data. This may involve opening another program (like Adobe Acrobat, OpenOffice), sending it to a plugin (like Flash), or displaying the text to the screen.
      • 2-level: The most common (and initial/original) way of understanding what to do with the URL request path (the /snowpants/send/ part), is to map this onto a filesystem, and return some file from some_internet_root_dir/snowpants/send/. Let's suppose that Servo's has a base web directory on Mac at Mac::/home/serve/www/ [9]. It uses this directory as the base for all file-serving requests. Then, if Servo were an Apache server, it would try to return an html response filled with the contents of Mac::/home/serve/www/snowpants/send/index.html .
      • 3-level: Servo, however, is a liberated, free-thinking, modern web server, written in Python. He lives in the city in his own apartment, has a good day job, he's making it on his own! Servo realizes that a url is just data , and he can respond however it makes sense to. Instead of trying to find a file, he is programmed to parse the url into a series of actions: send > snowpants. There is no directory called snowpants anywhere on Mac.
    5. Servo, the very model of a modern web server, is programmed to interpret the request URL as a series of directives (send > snowpants), and does a series of actions. It sends a text message to Minnie's phone, an email to the warehouse in San Diego, and builds a text response to send back to Minnie's computer. The text response consists of some html text describing Servo's actions, and exhorting Minnie to valiantly struggle on until the longjohn's arrive. It sends the response back to the IP address in the original request.
    6. The response comes back to Minneapolis. Brow interprets it correctly as html, and prints it to the screen, where Minnie sees it.

    Example URLs

    From 6pm.com, a shoe merchant:

    http://www.6pm.com/search/shoes/filter/productTypeFacet/"Shoes"/gender/"womens"/subCategoryFacet/"Knee+High"/size/"7"/colorFacet/"Brown"/categoryFacet/"Boots"/page/1

    From part of a url produced by a MapServer application:

    http://maps.work.com/cgi-bin/mapserv?map=/bucket/websites/htdocs.maps/mapfiles/mymap.map&mode=map&layers=State&map_imagetype=png&mapext=388109.29996044+4885946.6306564+553109.29996044+5067446.6306564&imgext=388109.29996044+4885946.6306564+553109.29996044+5067446.6306564&map_size=825+907&imgx=412.5&imgy=453.5&imgxy=825+907


    ExercisesEdit

    1. What are the patterns in these urls?
    2. Look at the 6pm example. What python data structure would be easy to map on the parts of this url?
    3. Look at the urlparse module in the standard library. Use the urlparse.urlparse and urlparse.parse_qs functions to parse these urls


    Web.py -- a Simple Web FrameworkEdit

    Web.py is a web framework written in Python that we will use for creating a web-based front end for our CYOA application. A web framework is a set of modules and functions that automate and simplify http request parsing and http response generation. Typically these frameworks include bits that handle language and filetype encoding, generating correct error codes, parsing url requests and other fiddly bits of that ilk. The website author is responsible for styling, content, user interaction and other site-specific bits.

    We are using web.py because it is very simple to set up, and has a low barrier to entry. Other frameworks may scale better, or have more features. For a simple web site, like the one we're making, we want to get into coding as quickly as possible.

    Hello World! in Web.pyEdit

    (These instructions slightly augment the ones at web.py, to ease installation for Windows users)

    1. Go to web.py.
    2. Download the tarball or zip-file of the web.py code. If you don't know what a tarball is, then you want the zipped code.
      1. (zip specific) Unzip the zipped webpy code using your favorite unzip utility. In Windows, you might have to double click on the file, or right-click and say "open with > compressed (zipped) folders)
      2. (tarball) tar -zxvf webpy.folder.name.tgz
    3. Create a project directory somewhere /path/to/webcyoa
    4. Copy the web subdirectory of the web.py code to your project directory
    5. cd /path/to/webcyoa
    6. Create a file code.py containing the contents of /Webpy_Hello_World
    7. From here, you're caught up with the web.py main page, and you can look for more help at the tutorial.
    8. Fire up the webapp!
      1. cd /path/to/webcyoa
      2. python code.py
        1. you should see something like: http://0.0.0.0:8080/. If so, your webserver is now running on Localhost (see below) and listening on port 8080.
        2. if you get an ImportError, then make sure you have the web folder in your project dir.
      3. Fire up your web-browser and type in http://localhost:8080
        • You should see "Hello World"
        • in your shell window, you should see something like: 127.0.0.1:1658 - - [31/Oct/2009 10:33:24] "HTTP/1.1 GET /" - 200 OK


    LocalhostEdit

    Who is localhost and what is he hosting, exactly? And why is it on my computer?

    localhost is simply the conventional name for you your computer. The ip address 127.0.0.1 is reserved for the local machine. 0.0.0.0 (for the observant) is special address which binds to all local interfaces with ip address. Details to review at your leisure, but the idea here is that it's running locally. Requests from outside your machine probably won't work.

    GET and POSTEdit

    The usual kind of http request is a GET address. When you submit a form on a page, it triggers a POST request. Thus if you wanted a webpy class to respond different when a form is submitted, give it a POST method. There are other http request types including PUT and DELETE that never really caught on in the wild.


    ExercisesEdit
    1. Research Python classes. Use your favorite search engine. Come with questions.
    2. Change the method of the hello.GET method, so that it prints "Hello, the current time is.... [now]" where [now] is the current time (cf: time.time())
    3. Explore how to serve static content, based on the tutorial.

    Lesson 6: Web1.0, MysteriousHouse.comEdit

    GoalsEdit

    1. The concept of interfaces
    2. making code abstract
    3. from command-line to webapp

    LessonEdit

    HomeworkEdit

    Lesson 7: Spicing things upEdit

    GoalsEdit

    1. expansion and hackability

    LessonEdit

    HomeworkEdit

Other PagesEdit

Additional ResourcesEdit

Exercises (under construction)Edit

References:Edit

  1. As of 9/9/09: choose the Python 2.6.2 Windows installer (Windows binary -- does not include source)
  2. automagically: hacker. automatically, and as if by magic. As in Fantasia, this can be a mixed blessing.
  3. In Python, valid identifiers start with a letter or _, then contain one or more numbers, non-whitespace chars, or digits.
  4. For gory details: Python Function Definitions
  5. the idea being, that the pseudocode notation should abstract away some of the python specific details, so that programmers can more easily talk about the meat of the problem, rather than the dressing
  6. Python maintains a semi-blessed repository for these at the Cheeseshop. Many other packages, such as Numeric Python, SqlAlchemy, Django, and NetworkX have their own sites.
  7. http://docs.python.org/library/stdtypes.html#truth-value-testing
  8. Seriously, take some time and look over stdlib before reinventing functions. Webservers, JSON, csv handling, regular expressions, imap handling, Sqlite database drivers, and a lot more.
  9. based on this path, Mac is a UNIX/Linux machine. On Windows, it might be something like C:/www/
Last modified on 19 October 2013, at 14:21