#!/usr/bin/env python
# $Id: pyspaceinvaders.py 1.51 2004/08/19 11:57:50 jimb Exp $
# Python Space Invaders.
# Author:   Jim Brooks  http://www.jimbrooks.org
# Date:     2004/08
# License:  GNU General Public License (GPL)
# Requires: Python 2.3, PyGame, SDL.
# Notes:    See notes.txt.
# ==============================================================================

import os, sys, random
import pygame
from pygame.locals import *
from pygame.rect import Rect
if not pygame.font: print "Warning, fonts disabled"
from libgame import *
from libutils import *

# Colors.
RGBbg        = (0, 0, 0)
RGBtext      = (210, 240, 255)
RGBalive     = (0, 255, 0)
RGBdead      = (255, 0, 0)
RGBgameover  = RGBdead
RGBgreen     = (0,255,0)
RGBcyan      = (0,200,200)
RGBgrey      = (64,64,64)

# ------------------------------------------------------------------------------
# Classes.
# ------------------------------------------------------------------------------

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Object class.
class Object:
    def __init__( self ):
        self.valid    = True
        self.hit      = 0
        self.movement = ( 0, 0 )
        return

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Player class.
class Player(Object):
    def __init__( self , st ):
        Object.__init__( self )
        self.image       = pygame.image.load( "img/player.png" )
        self.image2      = pygame.image.load( "img/playerb.png" )
        self.imageHit    = pygame.image.load( "img/explosion.png" )
        self.rect        = self.image.get_rect()
        self.imageFlip   = False
        self.step        = 7 * st.stride
        self.fire        = False
        self.fireLatency = 5
        self.salvo       = 3
        return

    def Reset( self, st, scr ):
        self.valid        = True
        self.hit          = 0 # counts down
        self.rect.centerx = scr.width / 2
        self.rect.bottom  = st.ground
        return        

    def Hit( self ):
        self.hit = 30
        st.playerLives -= 1
        if st.playerLives <= 0: # player out of lives?
            st.GameOver()
        return

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Alien class.
class Alien(Object):
    # constants:
    points    = 50
    colCnt    = 10
    rowCnt    = 4
    totalCnt  = colCnt * rowCnt
    # vars:
    imageFlip = False
    horzDir   = -1
    
    def __init__( self, fname, fname2 ):
        Object.__init__( self )
        self.image     = pygame.image.load(fname)
        self.image2    = pygame.image.load(fname2)
        self.imageHit  = pygame.image.load("img/explosion.png")
        self.rect      = self.image.get_rect()
        # reset vars
        self.imageFlip = False
        self.horzDir   = -1
        return
    
    def Hit( self ):
        self.hit = 5
        return

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Player missile class.
class PlayerMissile(Object):
    def __init__( self ):
        Object.__init__( self )
        self.image        = pygame.image.load( "img/missile_player.png" )
        self.rect         = self.image.get_rect()
        self.rect.centerx = st.player.rect.centerx
        self.rect.centery = st.player.rect.centery - st.player.rect.height
        return

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Alien missile class.
class AlienMissile(Object):
    def __init__( self, alien ):
        Object.__init__( self )
        self.image        = pygame.image.load( "img/missile_alien.png" )
        self.rect         = self.image.get_rect()
        self.rect.centerx = alien.rect.centerx + random.randint( -8, 8 )
        self.rect.centery = alien.rect.centery + alien.rect.height
        return

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Screen state
class Screen:
    # Initialize screen geometry and caption text in title bar.
    def __init__( self, w, h, caption ):
        self.width  = w
        self.height = h
        self.screen = pygame.display.set_mode( (w,h), pygame.FULLSCREEN )
        self.font   = pygame.font.SysFont( None, 28 )
        pygame.display.set_caption( caption )
        return

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Game state
class State:
    # Fundamental game states:
    state_over     = -2  # enum
    state_pause    = -1  # enum
    state_stop     = 0   # enum
    state_play     = 1   # enum
    state          = state_stop

    # Sprites:
    player         = None
    alienColumns   = []
    playerMissiles = []
    alienMissiles  = []

    # Misc:
    stride         = 1.0  # effective speed
    tick           = 0
    tempo          = 1
    score          = 0
    level          = 1
    cheat          = 0    # 1: 999 lives 2: 999 lives + persistent gunfire
    ground         = None
    ceiling        = None
    quitable       = 1
    
    # - - - - - - - - -
    # Initialize game state.
    # Pass Screen object as "scr".
    def __init__( self, scr ):
        self.Reset( scr, 1 )
        return

    # - - - - - - - - -
    # Reset game state.
    # Can advance level of difficulty.
    def Reset( self, scr, level ):
        self.tick     = 0
        self.tempo    = 1
        if self.state == self.state_over:
            self.score    = 0
        self.level    = level
        if not self.cheat or level == 1:
            self.playerLives = 5
        self.ground   = scr.height - 6
        self.ceiling  = 32

        msg.Del( MsgListGameOver() )

        # Create/reset player.
        if self.player == None:
            self.player = Player( self )
        self.player.Reset( self, scr )

        # Delete all aliens (invalidate then prune).
        if len(self.alienColumns):
            for col in range(Alien.colCnt):
                for alien in self.alienColumns[col]:
                    alien.valid = False
            PruneListList(self.alienColumns)
            
        # Create a full population of aliens.
        self.alienColumns = []
        for col in range(Alien.colCnt):
            self.alienColumns.append([])
            for row in range(Alien.rowCnt):
                if row == 0:
                    fname  = "img/enemy3.png"
                    fname2 = "img/enemy3b.png"
                elif row == 1:
                    fname  = "img/enemy2.png"
                    fname2 = "img/enemy2b.png"
                else:
                    if col % 2 == row % 2:
                        fname  = "img/enemy3.png"
                        fname2 = "img/enemy3b.png"
                    else:
                        fname  = "img/enemy2.png"
                        fname2 = "img/enemy2b.png"

                alien = Alien(fname, fname2)
                alien.rect.move_ip(col * 48 + 40, row * 48 + self.ceiling)
                self.alienColumns[col].append(alien)

        # Delete any missiles.
        if len(self.playerMissiles):
            for m in self.playerMissiles: m.valid = False
            PruneList(self.playerMissiles)
        if len(self.alienMissiles):
            for m in self.alienMissiles:  m.valid = False
            PruneList(self.alienMissiles)

        return

    # - - - - - - - - -
    def GameOver( self ):
        self.state = self.state_over
        msg.Add( MsgListGameOver() )
        splash.Enable(True)
        return

    # - - - - - - - - -
    # Toggle pause if playing else do nothing.
    def TogglePause( self ):
        if self.state == self.state_play:
            self.state = self.state_pause
        elif self.state == self.state_pause:
            self.state = self.state_play
        return

    # - - - - - - - - -
    # For convenience, return a single list
    # from the lists of alien columns.
    # NOTE: The list returned is a derivation.
    #       It shouldn't be modified (treat as read-only).
    def AlienList( self ):
        list = []
        for col in range(Alien.colCnt):
            for alien in self.alienColumns[col]:
                list.append( alien )
        return list

# ------------------------------------------------------------------------------
# Subroutines.
# ------------------------------------------------------------------------------

# Missile/missile collisions.
# Detects if one missile has collided into any opposite missiles (plural).
def MissileMissileCollision( missile, missileIdx, oppMissileList ):
    if missile.valid:
        oppMissileIdx = -1
        for oppMissile in oppMissileList:
            oppMissileIdx += 1
            if oppMissile.valid and Collided( missile, oppMissile ):
                missile.valid = False
                oppMissile.valid = False
    return

# Has any alien collided into the player?
def PlayerAlienCollision( alienList ):
    for alien in alienList:
        # Ignore further collisions while both are exploding.
        if alien.valid and Collided( alien, st.player ):
            if alien.hit <= 0 and st.player.hit <= 0:
                alien.Hit()
                st.player.Hit()
    return

# ------------------------------------------------------------------------------
# Render.
# ------------------------------------------------------------------------------

def Render():
    # Clear screen.
    scr.screen.fill( RGBbg )

    # Don't render if stopped.
    if st.state != st.state_stop:
        Render2()

    # Display any text messages (over whatever was rendered).
    msg.Render()

    pygame.display.flip()

    return

def Render2():
    # Blit player.
    if st.player.hit <= 0:
        if st.player.imageFlip:
            scr.screen.blit( st.player.image,  st.player.rect )
        else:
            scr.screen.blit( st.player.image2, st.player.rect )
    else:
        scr.screen.blit( st.player.imageHit, st.player.rect )

    # Blit aliens.
    for alien in st.AlienList():
        if alien.hit <= 0:
            if Alien.imageFlip:
                scr.screen.blit( alien.image, alien.rect )
            else:
                scr.screen.blit( alien.image2, alien.rect )
        else:
                scr.screen.blit( alien.imageHit, alien.rect )

    # Blit player missiles.
    for o in st.playerMissiles:
        if o.valid and o.rect.centery > 0:
            scr.screen.blit( o.image, o.rect )

    # Blit alien missiles.
    for o in st.alienMissiles:
        if o.valid and o.rect.centery < scr.height:
            scr.screen.blit( o.image, o.rect )

    # Show status.
    MsgShowScore()
    MsgShowLevel()
    MsgShowLives()

    return

# ------------------------------------------------------------------------------
# Animate.
# ------------------------------------------------------------------------------

# Main animation function.
# Assumes: invocation driven by timer tick.
def Animate():
    # Periodically prune invalid objects.
    PruneList( st.playerMissiles )
    PruneList( st.alienMissiles )
    PruneListList( st.alienColumns )

    # Animate only if playing.
    if st.state == st.state_play:
        alienList = st.AlienList()
        AnimatePlayer( alienList )
        AnimateAliens( alienList )
        PlayerAlienCollision( alienList )

    return

# Animate player.
def AnimatePlayer( alienList ):
    # Animate exploding player (ok if not hit).
    st.player.hit -= 1

    # Don't let player move while exploding.
    if st.player.hit <= 0:
        # Continue player movement until key released.
        if st.player.movement != (0,0):
            if st.player.rect.left      + st.player.movement[0] > 0 \
               and st.player.rect.right + st.player.movement[0] < scr.width:
                st.player.rect.move_ip( st.player.movement[0], 0 )
                st.player.imageFlip = not st.player.imageFlip
        # Player's gun has a latency period and a limited salvo.
        if st.player.fire and st.tick % st.player.fireLatency == 0 and len(st.playerMissiles) < st.player.salvo:
            st.playerMissiles.append( PlayerMissile() )

    # Animate missiles from player.
    missileIdx = -1
    for missile in st.playerMissiles:
        missileIdx += 1
        # Animate missile.
        missile.rect.centery -= 10 * st.stride
        # Off-screen?
        if missile.rect.top < st.ceiling:
            missile.valid = False # will be pruned later
            continue
        # Has player's missiles hit any alien?
        # Exclude any hit alien from further collision-detection
        # in order to allow player missiles to fly thru explosion
        # to hit a higher alien.
        for alien in alienList:
            if Collided( missile, alien ) and alien.valid and alien.hit <= 0:
                alien.Hit()
                st.score += Alien.points
                if st.cheat != 2: missile.valid = False
        # Has this missile hit any opposite missile?
        MissileMissileCollision( missile, missileIdx, st.alienMissiles )

    return

# Animate aliens (part 1).
def AnimateAliens( alienList ):
    # Animate aliens (unless player is hit/exploding).
    if st.player.hit <= 0:
        # Call actual routine.
        (invaded,alienCnt) = AnimateAliens2( alienList )
        # Game over if aliens have invaded (reached the ground).
        # Or were all aliens destroyed?  If so, goto next level.
        if invaded:
            st.GameOver()
        elif alienCnt == 0:
            st.Reset( scr, st.level + 1 )
    return

# Animate aliens (part 2).
# Returns: (invaded,alienCnt)
# "invaded" true if any alien has reached the ground.
def AnimateAliens2( alienList ):
    invaded = False

    # Animate the walking of aliens.
    if st.tick % 5 == 0:
        Alien.imageFlip = not Alien.imageFlip

    # Only aliens at bottom of columns can fire.
    for list in st.alienColumns:
        if len(list):
            alien = list[-1] # last element (alien at bottom of column)
            if random.randint( 0, 1000 ) > (999 - min(20,2*st.tempo)):
                st.alienMissiles.append( AlienMissile( alien ) )

    # Animate missiles from aliens.
    missileIdx = -1
    for missile in st.alienMissiles:
        missileIdx += 1
        # Animate missile.
        missile.rect.centery += 10 * st.stride
        # Off-screen?
        if missile.rect.bottom > st.ground:
            missile.valid = False # will be pruned later
            continue
        # Has an alien missile hit the player?
        # Ignore further hits while player explodes.
        if Collided( missile, st.player ) and st.player.hit <= 0:
            missile.valid = False
            st.player.Hit()
        # Has this missile hit any opposite missile?
        MissileMissileCollision( missile, missileIdx, st.playerMissiles )

    # Animate alien movement:
    # - right-to-left
    # - down
    # - left-right
    # - down
    # - repeat

    # Move all aliens left or right.
    for alien in alienList:
        alien.rect.centerx += Alien.horzDir * st.tempo * st.stride

    # Has any alien hit the left/right edge of screen?
    hitEdge = False
    padding = 4
    for alien in alienList:
        if alien.rect.left - padding < 0 or alien.rect.right + padding > scr.width:
            hitEdge = True
            break
    if hitEdge:
        # Reverse horizontal direction of aliens.
        Alien.horzDir = -Alien.horzDir
        # Move all aliens downward one step.
        for alien in alienList:
            alien.rect.centery += 4 * st.stride
            # Has this alien invaded (reached the ground)?
            if alien.rect.bottom >= st.ground:
                invaded = True

    # Decrement alien.hit of every alien
    # in order to animate exploding aliens.
    for alien in alienList:
        alien.hit -= 1
        # Subtle: An alien is dead if .hit was decremented from 1 to 0.
        # .hit is assigned a positive value if alien was struck.
        if alien.hit == 0:
            alien.valid = False

    # Increase tempo (alien movement) as the amount of aliens decreases.
    factor = Alien.totalCnt / 4
    st.tempo = min(10,st.level)
    if len(alienList) > 5:
        st.tempo += 4 - int( len(alienList) / factor ) + 1
    else:
        st.tempo += 12 # very quick when few aliens survive

    return (invaded,len(alienList))

# ------------------------------------------------------------------------------
# Specific messages.
# ------------------------------------------------------------------------------

# The reason these are functions is because (x,y) is computed dynamically
# based on screen size.  The functions return a list that contains one tuple.

def MsgListGameOver():
    return [ ( "GAME OVER", scr.width/4 * 3, 4, RGBgameover, 0 ) ]
#             ( "press 1 to restart!", scr.width/2, scr.height/2+32, RGBgameover, 1) ]

def MsgShowScore():
    msg.Show( ("SCORE: "+str(st.score), 4, 4, RGBtext, 0) )
    return
    
def MsgShowLevel():
    msg.Show( ("LEVEL: "+str(st.level), 16+scr.width/4.0*1.0, 4, RGBtext, 0) )
    return
    
def MsgShowLives():
    if st.playerLives:
        rgb = RGBalive
    else:
        rgb = RGBdead
    msg.Show( ("LIVES: "+str(st.playerLives), scr.width/4.0*2.0, 4, rgb, 0) )
    return

# ------------------------------------------------------------------------------
# Splash.
# ------------------------------------------------------------------------------

class Splash:
    def __init__( self ):
        self.on = True
        self.Enable( self.on )
        return

    def SplashList( self ):
        x = scr.width/2
        y = 8
        return [ ( "UBERVADERS",                  x, y+32*1, RGBgreen, 1 ), \
                 ( "1: start",                  x, y+32*3, RGBtext, 1 ), \
                 ( "Left Arrow: left",          x, y+32*4, RGBtext, 1 ), \
                 ( "Right Arrow: right",        x, y+32*5, RGBtext, 1 ), \
                 ( "CTRL: fire",                x, y+32*6, RGBtext, 1 ), \
                 ( "ESC: pause",                x, y+32*7, RGBtext, 1 ), \
                 ( "based on",                  x, y+32*13, RGBgrey, 1 ), 
                 ( "www.jimbrooks.org/web/python/pyspaceinvaders/",                  x, y+32*14, RGBgrey, 1 ) ]
#                 ( "F5, F6: CHEAT, CHEAT MORE", x, y+32*8, RGBtext, 1 ), \
#                 ( "Q: QUIT",                   x, y+32*9, RGBtext, 1 )  ]

    def Enable( self, f ):
        if f:
            msg.Add( self.SplashList() )
            self.on = True
        else:
            msg.Del( self.SplashList() )
            self.on = False
        return

    # If stopped, ESC key does nothing (nothing to unpause).
    def Toggle( self ):
        if st.state != st.state_stop:
            self.on = not self.on
            self.Enable(self.on)
        return


class DummyJoystick:
    def __init__(self):
        pass
    def init(self):
        return None
    def quit(self): 
        return None
    def get_init(self):
        return True
    def get_id(self):
        return 0
    def get_name(self):
        return 'dummy'
    def get_numaxes(self): 
        return 2
    def get_axis(self,dummy):
        return 0
    def get_numballs(self):
        return 0
    def get_ball(self,dummy):
        return 0
    def get_numbuttons(self): 
        return 2
    def get_button(self,dummy):
        return False
    def get_numhats(self):
        return 0
    def get_hat(self,dummy):
        return 0,0


# ------------------------------------------------------------------------------
# Initialization.
# ------------------------------------------------------------------------------

random.seed()
pygame.init()
POLLCLOCK = USEREVENT + 1
pygame.time.set_timer ( POLLCLOCK, 40 )
scr    = Screen( 640, 480, "Python Space Invaders" )
msg    = Msg( scr.screen, scr.font )
st     = State( scr )
splash = Splash()

pygame.mouse.set_visible(False)

## joystick-foo
try:
    pygame.joystick.init()
    joy = pygame.joystick.Joystick(0)
    joy.init()
except:
#    joy = DummyJoystick()
    joy = None

# ------------------------------------------------------------------------------
# Event loop.
# ------------------------------------------------------------------------------

run = True

while run:
    # - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # Process events.
    event = pygame.event.wait()
    # .......................................................
    if event.type == pygame.QUIT:        
        sys.exit()
    # .......................................................
    elif event.type == POLLCLOCK:
        st.tick += 1
        Animate()
        Render()
    # .......................................................
    
#    if joy.get_name() == 'dummy':
    if joy == None:
        if event.type == KEYDOWN:
            # Start game?
            if event.key == K_1:
                st.Reset( scr, 1 )
                st.state = st.state_play
                splash.Enable( False )
            # Quit?
            elif event.key == K_k and st.quitable == 1:
                pygame.mouse.set_visible(True)
                run = False
            # ESC?
            elif event.key == K_ESCAPE:
                splash.Toggle()
                st.TogglePause()
            
            #----ignore other keys if not playing----
            if st.state != st.state_play:
                continue
            #----ignore other keys if not playing----

            # Move player left?
            if event.key == K_a or event.key == K_LEFT:
                st.player.movement = ( -st.player.step, 0 )
            # Move player right?
            elif event.key == K_d or event.key == K_RIGHT:
                st.player.movement = ( st.player.step, 0 )
            # Player fired gun?
            elif event.key == K_RCTRL or event.key == K_LCTRL:
                st.player.fire = True
            # Cheat?
    #        elif event.key == K_F5:
    #            st.cheat = 1
    #            st.playerLives = 999
    #            st.player.salvo = 7
    #        elif event.key == K_F6:
    #           st.cheat = 2
    #            st.playerLives = 999
    #            st.player.salvo = 12
        # .......................................................
        elif event.type == KEYUP:
            # Stop moving player?
            if event.key == K_z or event.key == K_x or event.key == K_LEFT or event.key == K_RIGHT:
                st.player.movement = ( 0, 0 )
            # Player stopped firing gun?
            elif event.key == K_RCTRL or event.key == K_LCTRL:
                st.player.fire = False
        # - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    else:
        # Start game?
        if joy.get_button(1) == True:
            st.Reset( scr, 1 )
            st.state = st.state_play
            splash.Enable( False )
        
        # Move player left?
        if joy.get_axis(0) < -0.5:
            st.player.movement = ( -st.player.step, 0 )
        # Move player right?
        elif joy.get_axis(0) > 0.5:
            st.player.movement = ( st.player.step, 0 )
        # Player fired gun?
        elif joy.get_button(0) == True:
            st.player.fire = True
    # .......................................................
        # Stop moving player?
        elif abs(joy.get_axis(0)) < 0.5:
            st.player.movement = ( 0, 0 )
        elif joy.get_button(0) == False:
            st.player.fire = False

