# ################ #
#   ballistix.py   #
# ################ #
# by Meithan West  #
# 22 / Jan / 2009  #
# ################ #
 
# Game instructions:
# Shoot the squares that appear on the screen
# LEFT/RIGHT: change turrent angle
# UP/DOWN: increase/decrease power
# SPACE: fire!
# ESCAPE: quit
 
# If the game runs too slow/fast, tweak the variable 'speed'
# in the 'Settings' section below
 
from __future__ import division
from math import *
import pygame
import random
pygame.init()
 
# Definitions
black = (0, 0, 0)
white = (255, 255, 255)
blue = (0, 0, 255)
green = (0, 255, 0)
red = (255, 0, 0)
yellow = (255, 255, 0)
purple = (255, 0, 255)
pi = 3.14159
g = 9.8
 
# Settings
width = 800
height = 550
bgcolor = blue
speed = 1000
 
# ###################### #
# Cannon class definition #
# ###################### #
class Cannon:
 
  # Constructor
  def __init__(self, surface):
    self.surface = surface
    self.x = 15
    self.y = 65
    self.alpha = 30
    self.size = 30
    self.power = 70
    self.fired = False
    self.score = 0
    self.shots = 0
    self.charge = 1000
    self.draw(1)
 
  # Cannon shoots
  def shoot(self):
    self.fired = True
    self.shots += 1
    self.charge = 0
 
  # Keyboard input handler
  def key_event(self,event):
    if event.key == pygame.K_LEFT:
      if self.alpha < 90:
        self.draw(0)
        self.alpha += 1
        self.draw(1)
    elif event.key == pygame.K_RIGHT:
      if self.alpha > 0:
        self.draw(0)
        self.alpha -= 1
        self.draw(1)
    elif event.key == pygame.K_DOWN:
      if self.power > 40:
        self.power -= 1
    elif event.key == pygame.K_UP:
      if self.power < 120:
        self.power += 1
 
  # Drawing function"
  def draw(self,mode):
    size = self.size
    # base
    x1 = scrx(self.x-size/2)
    y1 = scry(self.y+size/2)
    x2 = scrx(self.x-size/2)
    y2 = scry(self.y)
    if mode == 0: color = bgcolor
    else: color = white
    pygame.draw.arc(self.surface, color, (x1,y1,size,size),0,3.2,1)
    pygame.draw.rect(self.surface, color, (x2,y2,size+1,size/2),1)
    # barrel
    x1 = scrx(self.x)
    y1 = scry(self.y)
    x2 = scrx(self.x + size*cos(self.alpha*pi/180))
    y2 = scry(self.y + size*sin(self.alpha*pi/180))
    if mode == 0: color = bgcolor
    else: color = yellow
    pygame.draw.line(self.surface, color, (x1,y1),(x2,y2),1)
 
# ###################### #
# Shell class definition #
# ###################### #
class Shell:
  
  # Constructor
  def __init__(self,surface,alpha,power):
    self.surface = surface
    self.size = 3
    # kinematics
    self.x0 = cannon.x + cannon.size*cos(alpha*pi/180)*1.1
    self.y0 = cannon.y + cannon.size*sin(alpha*pi/180)*1.1
    self.vx0 = power*cos(alpha*pi/180)
    self.vy0 = power*sin(alpha*pi/180)
    self.t0 = t
    self.x = self.x0
    self.y = self.y0
    self.vx = self.vx0
    self.vy = self.vy0
 
  # Function to move the shell
  # Uses exact solutions to the kinematic equations
  def move(self):
    self.undraw()
    self.x = self.x0 + self.vx0*(t-self.t0)
    self.y = self.y0 + self.vy0*(t-self.t0) - g/2*(t-self.t0)**2
    self.vx = self.vx0
    self.vy = self.vy0 - g*(t-self.t0)
    if not self.destroyed(): self.draw()
 
  # Check if shell is out of the playing area
  def destroyed(self):
    sx = self.x
    sy = height-self.y
    if sx > width or sy > height-55-self.size:
      return True
    else:
      return False
 
  # Drawing function
  def draw(self):
    # shell
    sx = scrx(self.x)
    sy = scry(self.y)
    pygame.draw.circle(self.surface,red,(sx,sy),self.size,0)
    # trail
    alpha = atan(self.vy/self.vx)
    cosa = cos(alpha)
    sina = sin(alpha)
    sx = scrx(self.x-self.size*cosa*1.3)
    sy = scry(self.y-self.size*sina*1.3)
    self.surface.set_at((sx,sy),red)
 
  # Erasing function
  def undraw(self):
    sx = scrx(self.x)
    sy = scry(self.y)
    pygame.draw.circle(self.surface,bgcolor,(sx,sy),self.size,0)
 
  # Utility function to return the shell position
  def pos(self):
    return self.x, self.y
 
# ####################### #
# Target class definition #
# ####################### #
 
class Target:
  
  # Constructor
  def __init__(self,surface):
    self.x = random.randint(50, width-10)
    self.y = random.randint(10, height-50)
    self.size = 10
    self.surface = surface
    self.draw()
    
  # Drawing function
  def draw(self):
    sx = scrx(self.x)
    sy = scry(self.y)
    pygame.draw.rect(self.surface,yellow,(sx,sy,self.size,self.size),0)
  
  # Erasing function
  def undraw(self):
    sx = scrx(self.x)
    sy = scry(self.y)
    pygame.draw.rect(self.surface,bgcolor,(sx,sy,self.size,self.size),0)
  
  # Checks if the target was hit by a shell
  def hit(self,(x,y),rad):
    if x > (self.x-rad) and \
       x < (self.x+self.size+rad) and \
       y > (self.y-self.size-rad) and \
       y < (self.y+rad):
      return True
    else: return False
 
# ###################### #
# Miscelaneous functions #
# ###################### #
 
# Draws the scoring board
def drawboard():
  pygame.draw.rect(screen,bgcolor,(0,height-49,width,49),0)
  printos(screen, "Power = " + str(cannon.power), 2, height-40, white, font)
  printos(screen, "Angle = " + str(cannon.alpha), 2, height-20, white, font)
  printos(screen, "Score = " + str(cannon.score), width/2-20, height-40, white, font)
  printos(screen, "Score = " + str(cannon.score), width/2-20, height-40, white, font)
  if cannon.shots == 0:
    printos(screen, "Accuracy = 0%", width/2-20, height-20, white, font)
  else:
    printos(screen, "Accuracy = " + str(int(cannon.score/cannon.shots*100)) + "%", width/2-20, height-20, white, font)
  # Reload indicator
  if cannon.charge == 1000:
    printos(screen, "Cannon: ", 100, height-40, white, font)
    printos(screen, "Ready", 160, height-40, green, font)
    pygame.draw.rect(screen,white,(100,height-21,100,10),1)
    pygame.draw.rect(screen,green,(101,height-20,cannon.charge/10-2,8),0)
  else:
    printos(screen, "Cannon: ", 100, height-40, white, font)
    printos(screen, "Reloading", 160, height-40, red, font)
    pygame.draw.rect(screen,white,(100,height-21,100,10),1)
    pygame.draw.rect(screen,red,(101,height-20,cannon.charge/10-2,8),0)
 
# Functions to convert game coordinates to screen coordinates
def scrx(x):
  return int(x)
def scry(y):
  return int(height-y)
 
# Generic screen printing function
def printos(surface, text, x, y, color, font):
  new = font.render(text, 1, color)
  surface.blit(new, (x, y))
 
# ################## #
# Main program start #
# ################## #
 
# Initialization
screen = pygame.display.set_mode((width,height))
clock = pygame.time.Clock()
pygame.key.set_repeat(25)
pygame.font.init()
font = pygame.font.Font(None, 20) 
screen.fill(bgcolor)
pygame.draw.line(screen, white, (0,height-50),(width,height-50), 1)
t=0
cannon = Cannon(screen)
target = Target(screen)
shell_list = []
drawboard()
running = True
 
# Main game loop
while running:
 
  # Event handler
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
      print "Thanks for playing!"
      running = False
    # Keyboard input
    elif event.type == pygame.KEYDOWN:
      # ESCAPE exits the game
      if event.key == pygame.K_ESCAPE:
        print "Thanks for playing!"
        running = False
      # SPACE shoots
      elif event.key == pygame.K_SPACE:
        if cannon.charge == 1000:
          cannon.shoot()
          shell_list.append(Shell(screen,cannon.alpha,cannon.power))
      # All other key events are handled by the Cannon class
      else:
        cannon.key_event(event)
 
  # For each shell
  if len(shell_list) > 0:
    for index,shell in enumerate(shell_list):
      shell.move()
      if shell.destroyed():
        shell_list.pop(index)
      if target.hit(shell.pos(),shell.size):
        cannon.score += 1
        shell.undraw()
        shell_list.pop(index)
        target.undraw()
        target = Target(screen)
 
  # Recharge cannon
  if cannon.charge < 1000: cannon.charge+=2
 
  # Draw frame
  drawboard()
  pygame.display.flip()
 
  # Update time
  t += 0.01
  clock.tick(speed)