# game2d.py
# Walker M. White (wmw2)
# November 14, 2013
"""Module to provide simple 2D game support.

This module provides all of the classes that are to use (or subclass) 
to create your game. DO NOT MODIFY THE CODE IN THIS FILE.  See the
online documentation in Assignment 5 for more guidance.  It includes
information not displayed in this module."""

# Basic Kivy Modules
import kivy
import kivy.app
import kivy.uix.label

# Lower-level kivy modules to support animation
from kivy.config import *
from kivy.clock import Clock
from kivy.graphics import *
from kivy.graphics.instructions import *
from kivy.config import Config

# Widgets necessary for some technical workarounds
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label

# Additional miscellaneous modules
import os
import os.path
import numpy
import random
import colormodel
import pygame.mixer
import sys

# User-defined resources
FONT_PATH  = str(os.path.join(os.path.dirname(__file__), 'Fonts'))
SOUND_PATH = str(os.path.join(os.path.dirname(__file__), 'Sounds'))
IMAGE_PATH = str(os.path.join(os.path.dirname(__file__), 'Images'))

import kivy.resources
kivy.resources.resource_add_path(FONT_PATH)
kivy.resources.resource_add_path(SOUND_PATH)
kivy.resources.resource_add_path(IMAGE_PATH)

# Initialize the sound engine.
FREQUENCY = 44100
BITSIZE   = -16
CHANNELS  = 2
BUFFER    = 1024
pygame.mixer.init(FREQUENCY,BITSIZE,CHANNELS,BUFFER)

#### CONSTANTS ####

# Data caching for graphics objects
CACHE_ALL    = 0
CACHE_POS    = 1
CACHE_SIZE   = 2
CACHE_COLOR  = 3
CACHE_SOURCE = 4

# LINE SIZE
LINE_SIZE = 1

#### HIDDEN HELPER FUNCTIONS ####
def  _same_side(p1, p2, a, b):
    """Return: True is p1, p2 are on the same side of segment ba.
    
    Precondition: p1, p2, a, b are all 2d tuples of int or float."""
    ba = numpy.append(numpy.subtract(b,a),[0])
    cp1 = numpy.cross(ba,numpy.subtract(p1,a))
    cp2 = numpy.cross(ba,numpy.subtract(p2,a))
    return numpy.dot(cp1,cp2) >= 0


def _in_triangle(p, t):
    """Return: True if p is in triangle t
    
    Precondition: p is a 2d tuple of int or float.
    t is a 6-element tuple of int or float."""
    return (_same_side(p, t[0:2], t[2:4], t[4:6]) and
            _same_side(p, t[2:4], t[0:2], t[4:6]) and
            _same_side(p, t[4:6], t[0:2], t[2:4]))


def _and(x,y):
    """Return: x and y (used for reduce)"""
    return x and y


def _is_num(x):
    """Return: True if x is an int or float"""
    return type(x) in [int,float]


def _is_color(x):
    """Return: True if x represents a color"""
    if type(x) in [colormodel.RGB, colormodel.HSV]:
        return True
    
    if type(x) in [tuple, list] and 3 <= len(x) <= 4:
        return reduce(_and, map(lambda x: type(x) in [int, float] and 0 <= x <= 1, x)) 
    
    return False


def _is_image_file(name):
    """Return: True if name is the name of an image file"""
    if type(name) != str:
        return False
    
    return os.path.exists(IMAGE_PATH+'/'+name)


def _is_font_file(name):
    """Return: True if name is the name of an font file"""
    if type(name) != str:
        return False
    
    return os.path.exists(FONT_PATH+'/'+name)


def _is_sound_file(name):
    """Return: True if name is the name of an font file"""
    if type(name) != str:
        return False
    
    return os.path.exists(SOUND_PATH+'/'+name)


#### FUNCTIONS ####

def Sound(filename):
    """Creates a new Sound object for the given file.
    
    This function is a proxy for the pygame.mixer.Sound class.  That class requires
    some finicky initialization in order to work properly.  In order to hide that from
    you, we have given you this function to use instead.  Treat this function just
    like a constructor (except that the object type is pygame.mixer.Sound, not Sound).
    
        :param filename: string providing the name of a sound file
    
    See the online documentation for more information."""
    assert _is_sound_file(filename), `filename`+' is not a sound file'
    absname = filename if os.path.isabs(filename) else str(os.path.join(SOUND_PATH, filename))
    return pygame.mixer.Sound(absname)


#### GEOMETRY CLASSES ####

class GPoint(object):
    """Instances are a Point in 2D space.
    
    This class is used primarily for recording and handling mouse locations."""
    
    # PROPERTIES 
    @property
    def x(self):
        """The x coordinate of the point.
        
        **Invariant**: Must be an int or float."""
        return self._x
    
    @x.setter
    def x(self,value):
        assert type(value) in [int, float], `value`+' is not a number'
        self._x = float(value)
    
    @property
    def y(self):
        """The y coordinate of the point.
        
        **Invariant**: Must be an int or float."""
        return self._y
    
    @y.setter
    def y(self,value):
        assert type(value) in [int, float], `value`+' is not a number'
        self._y = float(value)
    
    # METHODS
    def __init__(self, x=0, y=0):
        """**Constructor**: creates a new GPoint value (x,y).
        
            :param x: initial x value
            **Precondition**: value is an int or float.
        
            :param y: initial y value
            **Precondition**: value is an int or float.
        
        All values are 0.0 by default.        
        """
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        """**Returns**: True if self and other are equivalent GPoint. 
        
        This method uses numpy to test whether the coordinates are 
        "close enough".  It does not require exact equality for floats.
        
            :param other: value to compare against
        """        
        return (type(other) == GPoint and numpy.allclose(self.list(),other.list()))
    
    def __ne__(self, other):
        """**Returns**: True if self and other are not equivalent GPoint. 
        
            :param other: value to compare against
        """
        return not self == other
    
    def __str__(self):
        """**Returns**: Readable String representation of this GPoint. """
        return "("+str(self.x)+","+str(self.y)+")"
    
    def __repr__(self):
        """**Returns**: Unambiguous String representation of this GPoint. """
        return "%s%s" % (self.__class__,self.__str__())
    
    def list(self):
        """**Returns**: A python list with the contents of this GPoint."""
        return [self.x,self.y]
    
    def __add__(self, other):
        """**Returns**: the sum of self and other.
        
        The value returned has the same type as self (so it is either
        a GPoint or is a subclass of GPoint).  The contents of this object
        are not altered.
        
            :param other: tuple value to add
            **Precondition**: value has the same type as self.
        """
        assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`}
        result = copy.copy(self)
        result.x += other.x
        result.y += other.y
        return result
    
    def __sub__(self, other):
        """**Returns**: the vector from tail to self.
        
        The value returned is a GPoint representing a vector with this point at its head.
        
            :param other: the tail value for the new Vector
            **Precondition**: value is a Point object.
        """
        assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`}
        result = copy.copy(self)
        result.x -= other.x
        result.y -= other.y
        return result
    
    def __mul__(self, scalar):
        """**Returns**: the scalar multiple of self and other.
        
        The value returned is a new GPoint.  The contents of this GPoint
        are not altered.
        
            :param scalar: scalar to multiply by
            **Precondition**: value is an int or float.
        """
        assert (type(scalar) in [int,float]), "value %s is not a number" % `scalar`
        result = copy.copy(self)
        result.x *= scalar
        result.y *= scalar
        result.z *= scalar
        return result
    
    def __rmul__(self, scalar):
        """**Returns**: the scalar multiple of self and other.
        
        The value returned is a new GPoint.  The contents of this GPoint
        are not altered.
        
            :param scalar: scalar to multiply by
            **Precondition**: value is an int or float.
        """
        return self.__mul__(scalar)
    
    def interpolate(self, other, alpha):
        """**Returns**: the interpolation of self and other via alpha.
        
        The value returned has the same type as self (so it is either
        a GPoint or is a subclass of GPoint).  The contents of this object
        are not altered. The resulting value is 
        
            alpha*self+(1-alpha)*other 
        
        according to GPoint addition and scalar multiplication.
        
            :param other: tuple value to interpolate with
            **Precondition**: value has the same type as self.
        
            :param alpha: scalar to interpolate by
            **Precondition**: value is an int or float.
        """
        assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`}
        assert (type(alpha) in [int,float]), "value %s is not a number" % `alpha`
        return alpha*self+(1-alpha)*other
    
    def distanceTo(self, other):
        """**Returns**: the Euclidean distance from this point to other
        
            :param other: value to compare against
            **Precondition**: value is a Tuple3D object.
        """
        return numpy.sqrt((self.x-other.x)*(self.x-other.x)+
                          (self.y-other.y)*(self.y-other.y))


class GObject(object):
    """Instances provide basic geometry information for drawing to a `GView`
    
    You should never make a GObject directly.  Instead, you should use one 
    of the subclasses: GRectangle, GEllipse, GLine, GTriangle, GPolygon, GImage, 
    and GLabel."""
    
    # PROPERTIES 
    @property
    def x(self):
        """The horizontal coordinate of the left hand side.
        
        **Invariant**: Must be an int or float. It is equivalent to attribute `left`."""
        return self._x
    
    @x.setter
    def x(self,value):
        assert type(value) in [int, float], `value`+' is not a number'
        self._x = float(value)
        if self._cache_on:
            self._cache(CACHE_POS)
    
    @property
    def y(self):
        """The vertical coordinate of the bottom.
        
        **Invariant**: Must be an int or float. It is equivalent to attribute `bottom`."""
        return self._y
    
    @y.setter
    def y(self,value):
        assert type(value) in [int, float], `value`+' is not a number'
        self._y = float(value)  
        if self._cache_on:
            self._cache(CACHE_POS)
    
    @property
    def width(self):
        """The horizontal width of this shape. Positive values go to the right.
        
        **Invariant**: Must be an int or float.""" 
        return self._width
    
    @width.setter
    def width(self,value):
        assert type(value) in [int, float], `value`+' is not a number'
        self._width = float(value)
        if self._cache_on:
            self._cache(CACHE_SIZE)
    
    @property
    def height(self):
        """The vertical height of this shape. Positive values go up.
        
        **Invariant**: Must be an int or float.""" 
        return self._height
    
    @height.setter
    def height(self,value):
        assert type(value) in [int, float], `value`+' is not a number'
        self._height = float(value)
        if self._cache_on:
            self._cache(CACHE_SIZE)
    
    @property
    def center_x(self):
        """The horizontal center of this shape.
        
        **Invariant**: Must be an int or float. It is equivalent to `x+width/2`""" 
        return self._x+self._width/2.0
    
    @center_x.setter
    def center_x(self,value):
        # Width remains fixed.
        self.x = value - (self._width/2.0)
    
    @property
    def center_y(self):
        """The vertical center of this shape.
        
        **Invariant**: Must be an int or float. It is equivalent to `y+height/2`""" 
        return self._y+self._height/2.0
    
    @center_y.setter
    def center_y(self,value):
        # Width remains fixed.
        self.y = value - (self._height/2.0)
    
    @property
    def left(self):
        """The horizontal coordinate of the left hand side.
        
        **Invariant**: Must be an int or float. It is equivalent to attribute `x`."""
        return self._x
    
    @left.setter
    def left(self,value):
        self.x = value
    
    @property
    def right(self):
        """The horizontal coordinate of the right hand side.
        
        **Invariant**: Must be an int or float. It is equivalent to `x+width`."""
        return self._x + self._width
    
    @right.setter
    def right(self,value):
        self.x = value - self._width
    
    @property
    def bottom(self):
        """The vertical coordinate of the bottom.
        
        **Invariant**: Must be an int or float. It is equivalent to attribute `y`."""
        return self._y
    
    @bottom.setter
    def bottom(self,value):
        self.y = value
    
    @property
    def top(self):
        """The vertical coordinate of the bottom.
        
        **Invariant**: Must be an int or float. It is equivalent to `y+height`."""
        return self._y + self._height
    
    @top.setter
    def top(self,value):
        self.y = value - self._height
    
    @property
    def fillcolor(self):
        """The object fill color.
        
        Used to color the backgrounds or, in the case of solid shapes, the shape
        interior.
        
        **Invariant**: Must be a 4-element list of float between 0 and 1. If you 
        assign it a RGB or HSV object from module `colormodel`, it will convert
        the color for your automatically."""
        return self._fillcolor.rgba
    
    @fillcolor.setter
    def fillcolor(self,value):
        assert _is_color(value), `value`+' is not a valid color'
        if type(value) in [tuple, list] and len(value) == 3:
            value = list(value)+[1.0]
        elif type(value) in [colormodel.RGB, colormodel.HSV]:
            value = value.glColor()
        
        self._fillcolor = Color(value[0],value[1],value[2],value[3])
        if self._cache_on:
            self._cache(CACHE_COLOR)
        
    @property
    def linecolor(self):
        """The object line color.
        
        Used to color the foreground, text, or, in the case of solid shapes, the
        shape border.
        
        **Invariant**: Must be a 4-element list of float between 0 and 1. If you 
        assign it a RGB or HSV object from module `colormodel`, it will convert
        the color for your automatically."""
        return self._linecolor.rgba
    
    @linecolor.setter
    def linecolor(self,value):
        assert _is_color(value), `value`+' is not a valid color'
        if type(value) in [tuple, list] and len(value) == 3:
            value = list(value)+[1.0]
        elif type(value) in [colormodel.RGB, colormodel.HSV]:
            value = value.glColor()
        
        self._linecolor = Color(value[0],value[1],value[2],value[3])
        if self._cache_on:
            self._cache(CACHE_COLOR)
    
    def __init__(self,**keywords):
        """**Constructor**: creates a new GObject to support drawing.
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide
        it with a list of keyword arguments that initialize various
        attributes.  For example, to initialize the x position and
        the fill color, use the constructor call
        
            GObject(x=2,fillcolor=colormodel.RED)
        
        You do not need to provide the keywords as a dictionary.
        The ** in the parameter `keywords` does that automatically.
        
        Any attribute of this class may be used as a keyword. The
        argument must satisfy the invariants of that attribute. See
        the list of attributes of this class for more information."""
        # Set the properties.
        # Set cache check to correct value
        self._cache_on = False
        
        # Have to initialize size first
        self.width  = keywords['width']  if  'width' in keywords else 0.0
        self.height = keywords['height'] if 'height' in keywords else 0.0
                
        # Now (relative) position
        if 'x' in keywords:
            self.x = keywords['x']
        elif 'left' in keywords:
            self.left = keywords['left']
        elif 'center_x' in keywords:
            self.center_x = keywords['center_x']
        elif 'right' in keywords:
            self.right = keywords['right']
        else:
            self._x = 0.0
        
        if 'y' in keywords:
            self.y = keywords['y']
        elif 'bottom' in keywords:
            self.bottom = keywords['bottom']
        elif 'center_y' in keywords:
            self.center_y = keywords['center_y']
        elif 'top' in keywords:
            self.top = keywords['top']
        else:
            self._y = 0.0
        
        self.fillcolor = keywords['fillcolor'] if 'fillcolor' in keywords else (1.0,1.0,1.0,1.0)
        self.linecolor = keywords['linecolor'] if 'linecolor' in keywords else (0,0,0,1)
    
    def contains(self,x,y):
        """Return: True if this shape contains the point (x,y), False otherwise.
        
            :param x: x coordinate of point to check
            **Precondition**: an int or float
            
            :param y: y coordinate of point to check
            **Precondition**: an int or float
        
        This method always returns `False` for a `GObject`."""
        return False
        
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        pass
        
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        # No drawing, but turn on the cache
        if not self._cache_on:
            self._cache()
            self._cache_on = True


class GLine(GObject):
    """Instances represent a sequence of line segments
    
    The line is defined by the `points` attribute which is an (even) sequence
    of alternating x and y values. When drawn in a `GView` object, the line
    starts from one x-y pair in `points` and goes to the next x-y pair.  If 
    `points` has length 2n, then the result is n-1 line segments.
    
    The object uses the attribute `linecolor` to determine the color of the
    line.  The attribute `fillcolor` is unused (even though it is inherited
    from `GObject`)."""
    
    # PROPERTIES 
    @property
    def x(self):
        """The horizontal coordinate of the left hand side.
        
        **Invariant**: Immutable float. It is equivalent to attribute `left`."""
        return self._x
    
    @property
    def y(self):
        """The vertical coordinate of the bottom
        
        **Invariant**: Immutable float. It is equivalent to attribute `bottom`."""
        return self._y
    
    @property
    def width(self):
        """The horizontal width of this shape. Positive values go to the right.
        
        **Invariant**: Immutable float.""" 
        return self._width
    
    @property
    def height(self):
        """The vertical height of this shape. Positive values go up.
        
        **Invariant**: Immutable float.""" 
        return self._height
    
    @property
    def center_x(self):
        """The horizontal center of this shape.
        
        **Invariant**: Immutable float. It is equivalent to `x+width/2`""" 
        return (self._x+self._width)/2.0
    
    @property
    def center_y(self):
        """The vertical center of this shape.
        
        **Invariant**: Immutable float. It is equivalent to `y+height/2`""" 
        return (self._y+self._height)/2.0
    
    @property
    def left(self):
        """The horizontal coordinate of the left hand side.
        
        **Invariant**: Immutable float. It is equivalent to attribute `x`."""
        return self._x
    
    @property
    def right(self):
        """The horizontal coordinate of the right hand side.
        
        **Invariant**: Immutable float. It is equivalent to `x+width`."""
        return self._x + self._width
    
    @property
    def bottom(self):
        """The vertical coordinate of the bottom.
        
        **Invariant**: Immutable float. It is equivalent to attribute `y`."""
        return self._y
        
    @property
    def top(self):
        """The vertical coordinate of the bottom.
        
        **Invariant**: Immutable float. It is equivalent to `y+height`."""
        return self._y + self._height
        
    @property
    def points(self):
        """The sequence of points that make up this line.
        
        **Invariant**: Must be a sequence (list or tuple) of int or float. 
        The length of this sequence must be even with length at least 4."""
        return self._points
    
    @points.setter
    def points(self,value):
        assert type(value) in [tuple,list], `value`+' is not a tuple or list'
        assert len(value) % 2 == 0 and len(value) > 2, 'length '+`len(value)`+' is not the correct size'
        assert reduce(_and, map(_is_num,value)), `value`+' is not a tuple of numbers'
        self._points = tuple(value)
        if self._cache_on:
            self._cache(CACHE_ALL)
    
    def __init__(self,**keywords):
        """**Constructor**: creates a new sequence of line segments.
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide
        it with a list of keyword arguments that initialize various
        attributes. For example, to create a line from (0,0) to
        (2,3), use the constructor call
        
            GLine(points=[0,0,2,3])
        
        This class supports the same keywords as `GObject`, though many of 
        them are unused, as the position and size attributes are now all 
        immutable.  Position and size are computed from the list of points.
        Therefore `point` and `linecolor` are the two primary keywords
        used by this constructor."""
        self._cache_on = False
        self.points = keywords['points'] if 'points' in keywords else ()
        self.fillcolor = keywords['fillcolor'] if 'fillcolor' in keywords else (1,1,1,1)
        self.linecolor = keywords['linecolor'] if 'linecolor' in keywords else (0,0,0,1)
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        mxx = None
        mxy = None
        mnx = None
        mny = None
        
        xpos = True
        for p in self.points:
            if xpos:
                if mxx is None or mxx < p:
                    mxx = p
                if mnx is None or mnx > p:
                    mnx = p
            else:
                if mxy is None or mxy < p:
                    mxy = p
                if mny is None or mny > p:
                    mny = p
            xpos = not xpos
        
        self._x = mnx
        self._y = mny
        self._width  = mxx-mnx
        self._height = mxy-mny
        self._lcache = Line(points=self.points,cap='round',joint='round',close=True)
    
    def contains(self,x,y):
        """Return: True if this shape contains the point (x,y), False otherwise.
        
            :param x: x coordinate of point to check
            **Precondition**: an int or float
            
            :param y: y coordinate of point to check
            **Precondition**: an int or float
        
        This method always returns `False` as a `GLine` has no interior."""
        return False
    
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        # Invoke the cache
        GObject.draw(self,view)
        view.canvas.add(self._linecolor)
        view.canvas.add(self._lcache)


class GTriangle(GLine):
    """Instances represent a solid triangle.
    
    The triangle is defined sequence of three point. Just as with the `GLine` class
    (which is the parent of this class), it has an attribute `point` which represents
    this points as an even-length sequence of ints or floats.
    
    The interior (fill) color of this rectangle is `fillcolor`, while `linecolor`
    is the color of the border."""
    
    @property
    def points(self):
        """The sequence of points that make up this triangle.
        
        **Invariant**: Must be a sequence (list or tuple) of int or float. 
        The length of this sequence must be 6 (for 3 points)."""
        return self._points
    
    @points.setter
    def points(self,value):
        assert type(value) in [tuple,list], `value`+' is not a tuple or list'
        assert len(value) == 6, 'length '+`len(value)`+' does not have 6 elements'
        assert reduce(lambda x, y: x and y, map(_is_num,value)), `value`+' is not a tuple of numbers'
        self._points = tuple(value)
        if self._cache_on:
            self._cache(CACHE_ALL)
    
    def __init__(self,**keywords):
        """**Constructor**: creates a new solid triangle.
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide it with a 
        list of keyword arguments that initialize various attributes. For 
        example, to create a red triangle with vertices (0,0), (2,3), and (0,4),
        use the constructor call
        
            GTriangle(points=[0,0,2,3,0,4],fillcolor=colormodel.RED)
        
        As with `GLine` the position and size attributes of this class are all
        immutable.  They are computed from the list of points."""
        GLine.__init__(self,**keywords)
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        GLine._cache(self)
        size = 3
        vertices = ()
        for x in range(3):
            vertices += self.points[2*x:2*x+2]+(0,0)
        self._mcache = Mesh(vertices=vertices, indices=range(size), mode='triangle_strip')
    
    def contains(self,x,y):
        """Return: True if this shape contains the point (x,y), False otherwise.
        
            :param x: x coordinate of point to check
            **Precondition**: an int or float
            
            :param y: y coordinate of point to check
            **Precondition**: an int or float
        
        This method uses a standard test for triangle inclusion."""
        return _in_triangle((x,y),self._points)
    
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        GObject.draw(self,view)
        view.canvas.add(self._fillcolor)
        view.canvas.add(self._mcache)
        view.canvas.add(self._linecolor)
        view.canvas.add(self._lcache)


class GPolygon(GLine):
    """Instances represent a solid polygon.  
    
    The polygon is a triangle fan from the attribute `centroid` to the points in the
    attribute `points` (inherited from `GLine`). If you wish to construct a convex 
    polygon, simply store the outside border in `point`, and assign `centroid` any
    point inside the polygon.  If you do not assign a `centroid`, one will be computed
    by averaging all of the points on the boundary.
    
    We use this approach to define polygons as it allows us to avoid complex 
    tesselation algorithms."""
    
    @property
    def centroid(self):
        """The base of the triangle fan representing this polygon.  
        
        In practice it should be an interior point in the shape, preferably the centroid.
        However, this is not enforced.
        
        **Invariant**: Must be a sequence (list or tuple) of int or float. 
        The length of this sequence must be 2 (to represent one point)."""
        return self._centroid

    @centroid.setter
    def centroid(self,value):
        assert type(value) in [tuple,list], `value`+' is not a tuple or list'
        assert len(value) == 2, `value`+' does not have 2 elements'
        assert reduce(lambda x, y: x and y, map(_is_num,value)), `value`+' is not a list of numbers'
        self._centroid = tuple(value)
        self._cache()
        
    def __init__(self,**keywords):
        """**Constructor**: creates a new solid polyon
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide it with a 
        list of keyword arguments that initialize various attributes. For 
        example, to create a hexagon, use the constructor call
        
            GPolygon(points=[0.87,0.5,0,1,-0.87,0.5,-0.87,-0.5,0,-1,0.87,-0.5])
        
        As with `GLine` the position and size attributes of this class are all
        immutable. They are computed from the list of points. If the shape is
        not convex, you must specify a `centroid` to act as the anchor for
        the triangle fan.  Otherwise, the centroid is computed automatically."""
        GLine.__init__(self,**keywords)

        if 'centroid' in keywords:
            self.centroid = keywords['centroid']
        else:
            xpos = 0.0
            ypos = 0.0
            for i in range(len(self.points)):
                if i % 2 == 0:
                    xpos += self.points[i]
                else:
                    ypos += self.points[i]
            size = float(len(self.points)/2)
            self.centroid = (xpos/size, ypos/size)
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        GLine._cache(self)
        size = len(self.points)/2
        vertices = self.centroid+(0,0)
        for x in range(size):
            vertices += self.points[2*x:2*x+2]+(0,0)
        vertices += self.points[0:2]+(0,0)
        self._mcache = Mesh(vertices=vertices, indices=range(size+2), mode='triangle_fan')
    
    def contains(self,x,y):
        """Return: True if this shape contains the point (x,y), False otherwise.
        
            :param x: x coordinate of point to check
            **Precondition**: an int or float
            
            :param y: y coordinate of point to check
            **Precondition**: an int or float
        
        This method cycles through each triangle in the triangle fan and
        tests each triangle for inclusion."""
        found = False
        for i in xrange(4,len(self._points),2):
            t = self.centroid+self.points[i-4:i]
            found = found or _in_triangle((x,y),t)
        
        return found
    
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        GObject.draw(self,view)
        view.canvas.add(self._fillcolor)
        view.canvas.add(self._mcache)
        view.canvas.add(self._linecolor)
        view.canvas.add(self._lcache)


class GRectangle(GObject):
    """Instances represent a solid rectangle.
    
    The attributes x and y refer to the bottom left corner of the rectangle. The 
    interior (fill) color of this rectangle is `fillcolor`, while `linecolor`
    is the color of the border.
    
    For more fine-grained rectangle placement, you should make use of the attributes 
    `center_x`, `center_y`, `right`, and `top`, all inherited from `GObject`.  
    See that class for more information."""
    
    
    def __init__(self,**keywords):
        """**Constructor**: creates a new solid rectangle
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide it with a 
        list of keyword arguments that initialize various attributes. For 
        example, to create a red square anchored at (0,0), use the constructor call
        
            GRectangle(x=0,y=0,width=10,height=10,fillcolor=colormodel.RED)
        
        This class supports the all same keywords as `GObject`."""
        GObject.__init__(self,**keywords)
        self._scache = None
        self._lcache = None
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        if self._scache is None:
            self._scache = Rectangle(pos=(self.x, self.y), size=(self.width, self.height))
            self._lcache = Rectangle(pos=(self.x-LINE_SIZE,self.y-LINE_SIZE),size=(self.width+2*LINE_SIZE,self.height+2*LINE_SIZE))        
        elif style == CACHE_POS:
            self._scache.pos=(self.x, self.y)
            self._lcache.pos=(self.x-LINE_SIZE, self.y-LINE_SIZE) 
        elif style == CACHE_SIZE:
            self._scache.size=(self.width, self.height)
            self._lcache.size=(self.width+2*LINE_SIZE, self.height+2*LINE_SIZE)
        else:
            self._scache = Rectangle(pos=(self.x, self.y), size=(self.width, self.height))
            self._lcache = Rectangle(pos=(self.x-LINE_SIZE,self.y-LINE_SIZE),size=(self.width+2*LINE_SIZE,self.height+2*LINE_SIZE))
    
    def contains(self,x,y):
        """Return: True if this shape contains the point (x,y), False otherwise.
        
            :param x: x coordinate of point to check
            **Precondition**: an int or float
            
            :param y: y coordinate of point to check
            **Precondition**: an int or float
        
        This method uses a standard test for rectangle inclusion."""
        return (self.left <= x and x <= self.right and self.bottom <= y and y <= self.top)
    
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        # Invoke the cache
        GObject.draw(self,view)
        view.canvas.add(self._linecolor)
        view.canvas.add(self._lcache)
        view.canvas.add(self._fillcolor)
        view.canvas.add(self._scache)


class GEllipse(GRectangle):
    """Instances represent a solid ellipse.
    
    The ellipse is the largest one that can be drawn inside of a rectangle whose 
    bottom left corner is at (x,y), with the given width and height.  The interior 
    (fill) color of this ellipse is `fillcolor`, while `linecolor` is the color 
    of the border.
    
    For more fine-grained rectangle placement, you should make use of the attributes 
    `center_x`, `center_y`, `right`, and `top`, all inherited from `GObject`.  
    See that class for more information."""
    
    def __init__(self,**keywords):
        """**Constructor**: creates a new solid ellipse
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide it with a 
        list of keyword arguments that initialize various attributes. For 
        example, to create a red circle anchored at (0,0), use the constructor call
        
            GEllipse(x=0,y=0,width=10,height=10,fillcolor=colormodel.RED)
        
        This class supports the all same keywords as `GObject`."""
        GRectangle.__init__(self,**keywords)
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        if self._scache is None:
            self._scache = Ellipse(pos=(self.x, self.y), size=(self.width, self.height))
            self._lcache = Ellipse(pos=(self.x-LINE_SIZE,self.y-LINE_SIZE),size=(self.width+2*LINE_SIZE,self.height+2*LINE_SIZE))
        if style == CACHE_POS:
            self._scache.pos=(self.x, self.y)
            self._lcache.pos=(self.x-LINE_SIZE, self.y-LINE_SIZE) 
        elif style == CACHE_SIZE:
            self._scache.size=(self.width, self.height)
            self._lcache.size=(self.width+2*LINE_SIZE, self.height+2*LINE_SIZE)
        else:
            self._scache = Ellipse(pos=(self.x, self.y), size=(self.width, self.height))
            self._lcache = Ellipse(pos=(self.x-LINE_SIZE,self.y-LINE_SIZE),size=(self.width+2*LINE_SIZE,self.height+2*LINE_SIZE))
    
    def contains(self,x,y):
        """Return: True if this shape contains the point (x,y), False otherwise.
        
            :param x: x coordinate of point to check
            **Precondition**: an int or float
            
            :param y: y coordinate of point to check
            **Precondition**: an int or float
        
        This method is better than simple rectangle inclusion.  It checks that
        the point is within the proper radius as well."""
        if not GRectangle.contains(self,x,y):
            return False
        
        cx = self.center_x
        cy = self.center_y
        rx = self.width/2.0
        ry = self.height/2.0
        
        dx = (x-cx)*(x-cx)/(rx*rx)
        dy = (y-cy)*(y-cy)/(ry*ry)
        
        return (dx+dy) <= 1.0


class GImage(GRectangle):
    """Instance represents a rectangular image.
    
    The image is given by a JPEG, PNG, or GIF file whose name is stored
    in the attribute `source`.  Image files should be stored in the
    **Images** directory so that Kivy can find them without the complete
    path name.
    
    In this graphics object, the `linecolor` and `fillcolor` attributes
    are ignored.  The image is displayed as a rectangle whose bottom
    left corner is defined by the attributes `x` and `y`. If the attributes
    `width` and `height` do not agree with the actual size of the image, 
    the image is scaled to fit.
    
    If the image supports transparency, then this object can be used to
    represent irregular shapes.  However, the `contains` method still
    treats this shape as a rectangle.
    """
    @property
    def source(self):
        """The source file for this image.
        
        **Invariant**. Must be a string refering to a valid file."""
        return self._source

    @source.setter
    def source(self,value):
        assert value is None or _is_image_file(value), `value`+' is not an image file'
        self._source = value
        self._cache()
        
    def __init__(self,**keywords):
        """**Constructor**: creates a new rectangle image
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide it with a 
        list of keyword arguments that initialize various attributes. For 
        example, load the image `beach-ball.png`, use the constructor
        
            GImage(x=0,y=0,width=10,height=10,source='beach-ball.png')
        
        This class supports the all same keywords as `GRectangle`, though
        the color keywords are ignored."""
        GRectangle.__init__(self,**keywords)
        if 'source' in keywords:
            value =  keywords['source']
            assert value is None or _is_image_file(value), `value`+' is not an image file'
            self._source = value
        else:
            self._source = None
        self._scache = None
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        if self._scache is None:
            self._scache = Rectangle(pos=(self.x, self.y), size=(self.width, self.height), source=self._source)        
        elif style == CACHE_POS:
            self._scache.pos=(self.x, self.y)
        elif style == CACHE_SIZE:
            self._scache.size=(self.width, self.height)
        elif style == CACHE_SOURCE:
            self._scache.source = self._source
        else:
            self._scache = Rectangle(pos=(self.x, self.y), size=(self.width, self.height), source=self._source)        
    
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        # Invoke the cache
        GObject.draw(self,view)
        view.canvas.add(self._fillcolor)
        view.canvas.add(self._scache)


class GLabel(GRectangle):
    """Instances represent an (uneditable) text label
    
    The attribute `text` defines the content of this image.  Uses of
    the escape character '\\n' will result in a label that spans multiple
    lines.  The label includes both the text, and a rectangular backdrop
    with bottom left corner at `pos` and width and height `size`.  The
    background color of this rectangle is `fillcolor`, while `linecolor`
    is the color of the text.
    
    The text itself is aligned within this rectangle according to the
    attributes `halign` and `valign`.  See the documentation of these
    attributes for how alignment works.  There are also attributes
    to change the point size, font style, and font name of the text.
    The `size` attribute of this label will grow to ensure that the
    text will fit in the rectangle, no matter the font or point size.
    
    To change the font, you need a .ttf (TrueType Font) file in the
    Fonts folder; refer to the font by filename, including the .ttf.
    If you give no name, it will use the default Kivy font.  The
    `bold` attribute only works for the default Kivy font; for other
    fonts you will need the .ttf file for the bold version of that
    font.  See `ComicSans.ttf` and `ComicSansBold.ttf` for an example."""
    
    @property
    def font_size(self):
        """Size of the text font in points.
        
        **Invariant**: A positive number (int or float)"""
        return self._label.font_size

    @font_size.setter
    def font_size(self,value):
        assert type(value) in (int,float), `value`+' is not a number'
        self._label.font_size = value
        self._label.texture_update()

    @property
    def font_name(self):
        """File name for the .ttf file to use as a font
        
        **Invariant**: string referring to a .ttf file in folder Fonts"""
        return self._label.font_name

    @font_name.setter
    def font_name(self,value):
        assert _is_font_file(value), `value`+' is not a font name'
        self._label.font_name = value
        self._label.texture_update()

    @property
    def bold(self):
        """Boolean indicating whether or not the text should be bold.
        
        Only works on the default Kivy font.  Does not work on custom
        .ttf files.  In that case, you need the bold version of the
        .ttf file.  See `ComicSans.ttf` and `ComicSansBold.ttf` for
        an example.
        
        **Invariant**: boolean"""
        return self._label.bold

    @bold.setter
    def bold(self,value):
        assert type(value) == bool, `value`+' is not a bool'
        self._label.bold = value
        self._label.texture_update()

    @property
    def text(self):
        """Text for this label.
        
        The text in the label is displayed as a single line, or broken
        up into multiple lines in the presence of the escape character
        '\\n'. The `size` attribute of this label grows to make sure
        that the entire text fits inside of the rectangle.
        
        **Invariant**: string"""
        return self._label.text
    
    @text.setter
    def text(self,value):
        assert type(value) == str, `value`+' is not a string'
        self._label.text = value
        self._label.texture_update()

    @property
    def halign(self):
        """Horizontal alignment for this label.
        
        The text is anchored inside of the label rectangle on either the
        left, the right or the center.  This means that as the size of
        the label increases, the text will still stay rooted at that
        anchor.
        
        **Invariant**: one of 'left', 'right', or 'center'"""
        return self._halign
    
    @halign.setter
    def halign(self,value):
        assert value in ('left','right','center'), `value`+' is not a valid horizontal alignment'
        self._halign = value
        self._label.halign = value
        self._cache(CACHE_POS)

    @property
    def valign(self):
        """Vertical alignment for this label.
        
        The text is anchored inside of the label rectangle at either the
        top, the bottom or the middle.  This means that as the size of
        the label increases, the text will still stay rooted at that
        anchor.
        
        **Invariant**: one of 'top', 'bottom', or 'middle'"""
        return self._valign
    
    @valign.setter
    def valign(self,value):
        assert value in ('top','middle','bottom'), `value`+' is not a valid vertical alignment'
        self._valign = value
        self._label.valign = value
        self._cache(CACHE_POS)

    def __init__(self,**keywords):
        """**Constructor**: creates a new text label.
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide
        it with a list of keyword arguments that initialize various
        attributes. For example, to create a label containing the
        word 'Hello', use the constructor call
        
            GLabel(text='Hello')
        
        This class supports the same keywords as `GObject`, as well
        as additional attributes for the text properties (e.g. font
        size and name)."""
        if not 'fillcolor' in keywords:
            keywords['fillcolor'] = [0.0,0.0,0.0,0.0]
        
        GRectangle.__init__(self,**keywords)
        self._label = Label(**keywords)
        self._label.size_hint = (None,None)
        
        if 'halign' in keywords:
            self._halign = keywords['halign']
        else:
            self._halign = 'left'
            self._label._halign = 'left'

        if 'valign' in keywords:
            self._valign = keywords['valign']
        else:
            self._valign = 'bottom'
            self._label._valign = 'bottom'
        
        self._label.bind(texture_size=self._callback)

    def _callback(self,instance=None,value=None):
        """Workaround to deal with parameter requirements for callbacks"""
        self._cache()
    
    def _cache(self,style=CACHE_ALL):
        """Helper method to cache data to speed drawing. This method should be  
        overridden for specific drawing instructions."""
        if style == CACHE_POS and not self._scache is None:
            self._label.pos=(self.x, self.y)
            self._scache.pos = (self.x, self.y)
            return
    
        self._label.size = self._label.texture_size
        self._label.pos = (self.x, self.y)
        self._label.color = self._linecolor.rgba
        
        # Resize the outside if necessary
        width  = max(self._width,self._label.width)
        height = max(self._height,self._label.height)
   
        # Reset to horizontal anchor position.
        if self._halign == 'left':
            self._width = width
        elif self._halign == 'center':
            cx = self.center_x
            self._width = width
            self._x = cx-(self._width/2.0)
        else:
            right = self.right
            self._width = width
            self._x = right-self._width

        # Reset to vertical anchor position.
        if self._valign == 'top':
            top = self.top
            self._height = height
            self._y = top - self._height
        elif self._valign == 'middle':
            cy = self.center_y
            self._height = height
            self._y = cy - (self._height/2.0)
        else:
            self._height = height
        
        # Internal Horizontal placement
        if self._halign == 'left':
            self._label.x = self.x
        elif self._halign == 'center':
            self._label.center_x = self.center_x
        else: # 'ightr'
            self._label.right = self.right
        
        # Internal Vertical placement
        if self._valign == 'top':
            self._label.top = self.top
        elif self._valign == 'middle':
            self._label.center_y = self.center_y
        else: # 'bottom'
            self._label.y = self.y
        
        self._scache = Rectangle(pos=(self.x, self.y), size=(self.width, self.height))
    
    def draw(self,view):
        """Draw this shape in the provide view.
        
            :param view: view to draw to
            **Precondition**: an *instance of* `GView`
        
        Ideally view should be the one provided by `Game`."""
        # Invoke the cache
        GObject.draw(self,view)
        view.canvas.add(self._fillcolor)
        view.canvas.add(self._scache)
        view.canvas.add(self._label.canvas)


#### APPLICATION CLASSES ####

class GView(FloatLayout):
    """The view class for a `Game` application.
    
    You may need to access an instance of this class to draw `GObject` 
    instances.  However, you will never need to construct one.
    You should only use the one provided in the `view` attribute of
    `Game`. See class `Game` for more information."""
    
    @property
    def touch(self):
        """Return: The current (x,y) coordinate of the mouse, if pressed.
        
        This method only returns coordinates if the mouse button is pressed.
        If the mouse button is not pressed it returns None. The origin (0,0)
        corresponds to the bottom left corner of the application window.
        
        There is currently no way to get the location of the mouse when
        the button is not preseed.  This a limitation of Kivy.""" 
        if self._touch is None:
            return None
        
        return GPoint(self._touch.x,self._touch.y)
    
    def __init__(self):
        """**Initializer**: creates a new GView"""
        FloatLayout.__init__(self)
        self.bind(on_touch_down=self._capture_touch)
        self.bind(on_touch_move=self._capture_touch)
        self.bind(on_touch_up=self._release_touch)
        self._touch = None
    
    def _capture_touch(self,view,touch):
        """Helper method to respond (and grap) a mouse press"""
        self._touch = touch
        #self._touch.grab(self)

    def _release_touch(self,view,touch):
        """Helper method to respond (and release) a mouse release"""
        self._touch = None

    def _redraw(self):
        """Helper called to refresh the screen each animation frame"""
        self.canvas.clear()
        # WORKAROUND TO CLEAR PROPERLY
        if self.canvas.length() > 0:
            try:
                for i in self.canvas.children[:]:
                    self.canvas.remove(i)
                self.canvas.children = list()
            except:
                pass
        self.canvas.add(Color(1,1,1))
        self.canvas.add(Rectangle(pos=self.pos,size=self.size))


class Game(kivy.app.App):
    """Primary controller class for a simple game application."""
    
    @property
    def width(self):
        """The window width
        
        **Invariant**: Immutable float."""
        return self._wwidth
    
    @property
    def height(self):
        """The window height
        
        **Invariant**: Immutable float."""
        return self._wheight

    @property
    def fps(self):
        """Target animation FPS
        
        We cannot guarantee that the FPS is achievable.  Python is not super
        fast. We do try for 60 FPS, however.
        
        **Invariant**: Immutable float > 0."""
        return self._fps
    
    @property
    def view(self):
        """The Game view.
        
        Pass this attribute to the `draw` method of a `GObject` instance 
        to draw it.
        
        Invariant**: Immutable instance of GView."""
        return self._view
    
    def __init__(self,**keywords):
        """**Constructor**: creates, but does not start, a new game.
        
            :param keywords: dictionary of keyword arguments 
            **Precondition**: See below.
        
        To use the constructor for this class, you should provide it with a 
        list of keyword arguments that initialize various attributes. The
        primary user defined attributes are the window width and height.
        For example, to create a game that fits inside of a 400x400
        window, the constructor
        
            Game(width=400,height=400)
        
        The game window will not show until you start the game.
        To start the game, use the method `run()`."""
        w = keywords['width']  if  'width' in keywords else 0.0
        h = keywords['height'] if 'height' in keywords else 0.0
        f = keywords['fps']    if 'fps'    in keywords else 60.0

        assert type(w) in [int, float], `w`+' is not a number'
        assert type(h) in [int, float], `h`+' is not a number'
        assert type(f) in [int, float], `f`+' is not a number'
        assert f > 0.0, `f`+' is not positive'
        self._wwidth = w
        self._wheight = h
        self._fps = f
        Config.set('graphics', 'width', str(self.width))
        Config.set('graphics', 'height', str(self.height))
        
        # Tell Kivy to build the application
        kivy.app.App.__init__(self,**keywords)
    
    def build(self):
        """Special Kivy method to initialize the graphics window"""
        self._view = GView()
        self._view.size_hint = (1,1)
        return self.view
    
    def _startup(self,dt):
        """Called to initialize.
        
        This is a callback-proxy for method init().  It handles
        important issues behind the scenes."""
        Clock.schedule_interval(self._refresh,1.0/self._fps)
        self.init()
    
    def _refresh(self,dt):
        """Called every animation frame.
        
            :param dt: time in seconds since last update
            **Precondition**: a number (int or float)
        
        This is a callback-proxy for method update().  It handles
        important issues behind the scenes."""
        self.view._redraw()
        self.update(dt)
        self.draw()
    
    def run(self):
        """Display the game window and start the game"""
        Clock.schedule_once(self._startup,-1)
        kivy.app.App.run(self)
    
    def stop(self):
        """Close the game window and exit Python.
        
        You should never need to call this"""
        kivy.app.App.stop(self)
        sys.exit(0)
    
    def init(self):
        """Initialize the game state.
        
        This method is distinct from the built-in initializer __init__.
        This method is called once the game is running.  You should use
        it to initialize any game specific attributes."""
        pass
    
    def update(self,dt):
        """Called every animation frame.
        
            :param dt: time in seconds since last update
            **Precondition**: a number (int or float)
        
        This method is called 60x a second to provide on-screen animation.
        Think of it as the body of the loop.  It is best to have fields
        that represent the current animation state so that you know where
        you are in the animation."""
        pass
    
    def draw(self):
        pass
