Friday, December 14, 2012

#Python: interactive programming

Coursera Python class

I've just finished a very interesting and fun Python class on Coursera[1]. "An Introduction to Interactive Programming in Python" is an extremely well prepared class with video lectures, quizzes, and great weekly mini-projects.

I don't play computer games, but I recommend this class to anyone interested in learning Python, reviewing object oriented and event-driven programming, or just having fun writing small interactive games in a cloud hosted coding environment written for the class - CodeSkulptor. In addition to CodeSkulptor, instructors also provide code templates that will help you learn good coding and design practices and graphic templates that cut down tedious parts of game development to a minimum, so that you can focus on having fun with interesting challenges of development.

Solution samples

To play a game open the link and click play button in the upper right corner. Another interesting thing about the class and Coursera in general is that after each project deadline you review five random people's work and yours is reviewed by five people. All of my mini-projects got graded at 100/100 :) This allows instructors assign homework in a class of 20,000+ students, but is also a great way for you to learn more, by analyzing code and seeing different approaches to the same problem you've just solved.

  • Asteroids: The last project was implementation of Asteroids, a classic game. To see the full code and play the game open the link below and click the play button in the upper left corner. Because of sound (mp3) it's best to open in Chrome web browser.
Controls: left and right arrows rotate the ship. Up arrow activates the engines. Space bar fires missiles.

http://www.codeskulptor.org/#user7-eyvNtFJtXM-0.py


  • Blackjack: During a hand of Blackjack, the players plays against a dealer with the goal of building a hand whose value is higher than the value of the dealer's hand, but not over 21. (The player is said to have busted in this case). Cards in Blackjack have the following values: an ace may be valued as either 1 or 11 (player's choice), face cards are valued at 10 and the value of the remaining cards corresponds to their number. 
The game logic for this simplified version of Blackjack is as follows. The player and the dealer are each dealt two cards initially with one of the dealer's cards being dealt faced down (his hole card). The player may then ask for the dealer to repeatedly "hit" his hand by dealing him another card. If at any point the value of the player's hand exceeds 21, the player is "busted" and loses immediately. At any point prior to busting, the player may "stand" and the dealer will then expose his hole card and hit his hand until the value of his hand is 17 or more. (Aces count as 11 unless it causes the dealer's hand to bust). If the dealer busts, the player wins. Otherwise, the player and dealer then compare the values of their hands and the hand with the higher value wins. The dealer wins ties in this version.

http://www.codeskulptor.org/#user6-plsVbiBJKx-145.py

  
  • Memory game: Memory is a card game in which the player deals out a set of cards face down.  In one turn, the player flips over two cards. If they match, the player leaves them face up.  If they don't match, the player flips the cards back face down.  The goal of Memory is to end up with all of the cards flipped face up in the minimum number of turns. For this project, the model for Memory is fairly simple - a memory deck consists of eight pairs of matching cards.Click on a card to uncover:
http://www.codeskulptor.org/#user5-onPaUpvPv6-23.py


  • Pong: Pong is one of the first arcade video games. Most work was drawing the playing field, including the paddles. To control the paddles use up and down arrows for the right paddle and "w" and "s" for the left one:
http://www.codeskulptor.org/#user5-Pj6I9i2QNHTg6xc-20.py


Comments on the code
  1. Asteroids:

Some comments on the most interesting parts of the code:
  • The game uses event driven programming to handle mouse clicks and keyboard pressing and releasing. 
  • Game objects are drawn on a canvas by a draw procedure that is executed 60 times per second. That procedure runs most of the game - updates positions of objects, redraws them in new positions etc.
  • Objects on the screen (rocks, missiles) are instances of class Sprite.
a_rock = Sprite(rock_pos, rock_vel, 0, rock_avel, asteroid_image, asteroid_info)
  • Make sure new rocks don't get created too close to the ship:
rock_pos = [random.randrange(0, width), random.randrange(0, height)]
        # keep picking a new position until you find the right one
        while dist(rock_pos, my_ship.get_position()) < MIN_SHIP_TO_NEW_ROCK_DIST:
            #print "too close", rock_pos
            rock_pos = [random.randrange(0, width), random.randrange(0, height)

  • Rocks move at random velocity and direction and speed of rotation:
rock_vel = [random.random() * .7 - .3, random.random() * .7 - .3]
rock_avel = random.random() * .2 - .1

  • Rocks, missiles, and even explosions are stored in sets, unordered collections of unique elements. Methods used on sets were limited to .add(), .remove(), and .difference_update(). The last one is interesting - I used it to remove elements from a set. For example, you first you go through a set of missiles on the screen, and add the ones whose age is higher than a threshold to a "remove" set. Once the remove set is ready you simply apply the .difference_upate(to_be_removed_set) to the set of missiles, and done.
set_rocks = set([])
missile_group = set([])
explosion_group = set([])

set_rocks.add(a_rock)
(...)
# empty set initialized
missile_remove = set([])
  # go through the set of missile on the screen
  for a_missile in missile_group:

      # identify the aged ones
      if a_missile.get_age() > MISSILE_AGE:
          missile_remove.add(a_missile)
  # actual removal
  missile_group.difference_update(missile_remove)

  • Modulo arithmetic used to make sure objects don't go outside the screen area:
# update position
self.pos[0] = (self.pos[0] + self.vel[0]) % width
self.pos[1] = (self.pos[1] + self.vel[1]) % height

  • Modulo arithmetic was also used to calculate indexes of sub images in a tiled 9x9 field "sprite." Explosion animations are 81 small blocks taken out of one large explosion image:
 http://commondatastorage.googleapis.com/codeskulptor-assets/explosion.hasgraphics.png

Modulo is used to get indexes of sub-images in format [0,1] ... [0, 8] in the first row, up to [8,8] in the last column of the last row. To get progress in animation a counter self.age is maintained in the explosion object and increased by one on each refresh of the screen:
EXPLOSION_DIM = [9, 9]
# aging is done in the update handler
explosion_index = [self.age % EXPLOSION_DIM[0], (self.age // EXPLOSION_DIM[0]) % EXPLOSION_DIM[1]]
#print explosion_index, self.age
canvas.draw_image(explosion_image2, [self.image_center[0] + explosion_index[0] * self.image_size[0],
                     self.image_center[1] + explosion_index[1] * self.image_size[1]],
                     self.image_size, self.pos, self.image_size)

  • Using cos() and sin() to convert angle of the ship to a vector used to propel it in that direction:
acc = angle_to_vector(self.angle)
(...)
# helper functions to handle transformations
def angle_to_vector(ang):
    return [math.cos(ang), math.sin(ang)]

  • Calculating distance between two points. Used to check for collisions (collision is when distance between two centers of objects is less than or equal to the sum of radii of circles enclosing the objects:
def dist(p,q):
    return math.sqrt((p[0]-q[0])**2+(p[1]-q[1])**2)

 (...)
    def collide(self, other_object):
        ''' detect and handle collisions. '''
        # how far from both objects' centers
        distance = dist(self.get_position(), other_object.get_position())
        if distance <= self.radius + other_object.get_radius():
            # there was a collision
            return True
        return False

  • Friction slows down the ship continuously, so when thrust not applied it will eventually stop:
self.vel[0] *= .99
self.vel[1] *= .99

  • To check if you clicked within the initial splash screen:
# center of the screen canvas
center = [width / 2, height / 2]
# size of the splash image
size = splash_info.get_size()

# true or false values if within the (x,y)
inwidth = (center[0] - size[0] / 2) < pos[0] < (center[0] + size[0] / 2)
inheight = (center[1] - size[1] / 2) < pos[1] < (center[1] + size[1] / 2)
if (not started) and inwidth and inheight:
        started = True

  • Images are "wrapped into" objects to make drawing easier:
class ImageInfo:
    def __init__(self, center, size, radius = 0, lifespan = None, animated = False):
        self.center = center
        self.size = size
        self.radius = radius
       (...)
            self.lifespan = lifespan

  • ImageInfo is used later to create an instance for each image:
explosion_info2 = ImageInfo([50, 50], [100, 100], 50, 24, True)
# and load the actual png
explosion_image2 = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/explosion.hasgraphics.png")

   
  • This way when images are later loaded in instances of objects (missiles, rocks, or the ship itself - called Sprites), all you have to do is pull image info from that class:
class Sprite:
    def __init__(self, pos, vel, ang, ang_vel, image, info, sound = None):
# (x, y) position on the screen
        self.pos = [pos[0],pos[1]]

# (x, y) change values that will increment position on every refresh of the screen
        self.vel = [vel[0],vel[1]]

# starting rotation (usually 0 radians)
        self.angle = ang

# important thing: rate of change of the rotation angle
        self.angle_vel = ang_vel

# This is all from the imageinfo class
        self.image = image
        self.image_center = info.get_center()
        self.image_size = info.get_size()
        self.radius = info.get_radius()
        self.lifespan = info.get_lifespan()
        self.animated = info.get_animated()
        self.age = 0


2. Blackjack:

  • Cards are implemented using lists and a dictionary that maps cards to values:
# define globals for cards. Clubs, Spades, Hearts, Diamonds.
SUITS = ('C', 'S', 'H', 'D')

# Ace, 2, etc. Notice 10 is 'T'.
RANKS = ('A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K')

# here is how we treat values - ace is worth one, but in the game we check if possible to count for 11 and do if yes.
VALUES = {'A':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, 'T':10, 'J':10, 'Q':10, 'K':10}


  • Desk is initialized as a list of cards shuffled and used to deal. Notice how two loops can be collapsed to one line in Python:
# define deck class
class Deck:
    def __init__(self):
        # the longer method
        #self.deck = []
        #for suit in SUITS:
        #    for rank in RANKS:
        #        #print suit, rank
        #        crd = Card(suit, rank)
        #        self.deck.append(crd)
        # initialize deck without two loops
        self.deck = [Card(suit, rank) for suit in SUITS for rank in RANKS]  
        # shuffle right after initialization
        self.shuffle()

  • Single card is simply a rank and a suit stored as strings:
class Card:
    def __init__(self, suit, rank):
        # check for allowed values
        if (suit in SUITS) and (rank in RANKS):
            # assign
            self.suit = suit
            self.rank = rank

  •  Hand is a list of card objects. Python can store objects this way. The only catch is that to print the content of a list of objects you'll need a __str__ method in the card object.
class Hand:
    def __init__(self, owner):
        # hand is a list of cards. Start empty.
        self.cards = []
        # sometimes we'll need to cover the second card in dealer's hand
        self.cover_second = False


  •  Deck
  • # define deck class
    class Deck:
        def __init__(self):
            # the longer method
            #self.deck = []
            #for suit in SUITS:
            #    for rank in RANKS:
            #        #print suit, rank
            #        crd = Card(suit, rank)
            #        self.deck.append(crd)
            # initialize deck using collection with two loops
            self.deck = [Card(suit, rank) for suit in SUITS for rank in RANKS]  
            # shuffle right after initialization
            self.shuffle()
  • Here is how we handle counting Aces as either 1 or 11. The fact that if two aces are counted as 11 you'll bust, the only option is how to count one ace, if present:
        if self.count_aces() == 0:
            return value
        # there is at least one ace
        else:
            # don't count that one ace as 11 if doing so would cause a bust
            if value + 10 > 21:
                return value
            # we are assuming if you can you want to count one ace as 11. Notice, we already
            # counted that ace as 1, so we only need to add 10.
            else:
                return value + 10


3. Memory game: 

  • Elegant way to generate a list of integers 0-7, each number exactly twice:
# using modulo to always get number in the range 0-7. Reminder never equal to the divisor 8.
# this also guarantees we will have two of each number (two 0, two 1, two 2...)
numbers = [i % 8 for i in range(16)]

  • Which cards should be flipped is controlled by a list of Boolean values:
# is nth card exposed? We start with none exposed.
exposed = [False for i in range(16)]

  • x coordinates of each click are converted to the field number:
# use integer division to calculate what field the user clicked on. 0...15
field_number = pos[0] // FIELD_WIDTH


4. Pong:

  • Good choice of constants was critical for later drawing and updating paddles:
PAD_WIDTH = 8
PAD_HEIGHT = 80
HALF_PAD_WIDTH = PAD_WIDTH / 2
HALF_PAD_HEIGHT = PAD_HEIGHT / 2

  • Drawing the paddles took some thinking. Paddles were implemented as a single number, the y coordinate of the center of each paddle on the screen, and the rest of the paddle was drawn and redrawn around that point.


1: http://www.coursera.org/
2: http://www.codeskulptor.org/

1 comment:

Gregory said...

I just finished taking this class and I agree. It was great. I learned a lot and I really enjoyed it.