# /***************************************************************************
# *            billiard.py
# *
# *  Mon Jul 12 20:21:14 2004
# *  Copyright  2004  Matias Grana,   matiasg@dm.uba.ar
# *              and  Stas Z,         stas@linux.isbeter.nl
# ****************************************************************************/
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, write to the Free Software Foundation,
# 675 Mass Ave, Cambridge, MA 02139, USA.

# Mandatory for a childsplay game.
RCFILE = 0

import os,sys,operator,random,string,locale
import pygame
from pygame.constants import *
from utils import load_image,load_sound,MyError,get_files, load_music,font2surf,\
    ChildsplayGoodies
from SpriteUtils import CPSprite,CPGroup,CPinit
from math import sqrt,acos

class Img:
    pass

class Misc:
    pass

class Sound:
    pass

class Hole:
    pass

class Ball(CPSprite):
    """
    A ball that can move on the table, collide with other ball and enter the hole.
    """
    def __init__(self, pic, start, kind):
        CPSprite.__init__(self)# embed it in this class, eg make CPSprite part of Ball
        self.image = pic
        self.rect = self.posint = pic.get_rect().move(start)
        self.posreal = (self.posint[0], self.posint[1])
        self.size = (self.posint.right - self.posint.left, self.posint.bottom - self.posint.top)
        self.sqradius = (self.size[1]/2)**2
        self.smthns = .95  #between 0 and 1. The higher, the more the ball will roll
        self.direc = (0,0)
        self.thkns = .9  #transmission of speed in collision
        self.kind = kind# we use this to test for red or blue ball. See _made_hole
    
    def __str__(self):
        return self.kind# now we can do "if str(ball object) == "red":". i don't know if we neede this :-)
    
    def on_update(self, *args):
        """ This is called on every refresh call."""
        v = self.madehole()
        return v# we use this to return 1 to the refresh call
    def moveit(self):
        """ Moves the ball """
        # It is important that the position be a real number. Otherwise the movement is not nice.
        # We cast it to int to blit it.
        self.posreal = (self.posreal[0]+self.direc[0], self.posreal[1]+self.direc[1])
        self.posint.left, self.posint.top = int(self.posreal[0]), int(self.posreal[1])
        # Don't fall out of the table!
        if self.posint.right > 792:
            self.posint.left = 792 - self.size[0]
            self.posreal = (self.posint.left, self.posreal[1])
            self.direc = (-self.direc[0],self.direc[1])
        if self.posint.bottom > 492:
            self.posint.top = 492 - self.size[1]
            self.posreal = (self.posreal[0], self.posint.top)
            self.direc = (self.direc[0],-self.direc[1])
        if self.posint.left < 8:
            self.posint.left = 8
            self.posreal = (8, self.posreal[1])
            self.direc = (-self.direc[0],self.direc[1])
        if self.posint.top < 8:
            self.posint.top = 8
            self.posreal = (self.posreal[0], 8)
            self.direc = (self.direc[0],-self.direc[1])
        # Slow down a bit!
        self.direc = (self.direc[0] * self.smthns, self.direc[1] * self.smthns)
        # If it's almost stopped, just stop it.
        if self.direc[0]**2 + self.direc[1]**2 < 1:
            self.direc = (0,0)
            return 0
        return 1
    def addtodir(self, x, y):
        """ changes the direction of the ball """
        self.direc = (self.direc[0]+x, self.direc[1]+y)
        return self.direc[0]**2+self.direc[1]**2
    def addtopos(self, x, y):
        """ changes position of the ball """
        self.posreal = (self.posreal[0]+x, self.posreal[1]+y)
        self.posint.left += x
        self.posint.top += y
    def callback(self, mousepos, mousebut):
        """ What to do under mouse button pressed.
         This is not a callback like the ones used in a typical CPSprite object."""
        diff = self.posint.move(self.size[0]/2-mousepos[0],self.size[1]/2-mousepos[1])
        dist = sqrt(diff[0]**2+diff[1]**2)
        # see if the kick was inside the ball and out from the center. If yes, change direction.
        if (0 < dist <= self.size[0]/2):
            valtoreturn = 1
            if (mousebut == 1):
                self.direc = (diff[0] / dist,  diff[1] / dist)
        #load_music(Sound.throw).play()
        else:
            valtoreturn = 0
        return valtoreturn
    def checkcollision(self, otherobj):
        """ checks whether two objects collide """
        x,y = (self.posreal[0] - otherobj.posreal[0], self.posreal[1] - otherobj.posreal[1])
        squaredist = x**2 + y**2
        dist = sqrt(squaredist)
        # check if both objects collide. If yes, change their directions.
        overlap = dist - (self.size[0] + otherobj.size[0]) / 2
        if overlap < 0:
            diffvel = (self.direc[0] - otherobj.direc[0], self.direc[1] - otherobj.direc[1])
            coef = self.thkns * (x*diffvel[0] + y*diffvel[1]) / squaredist
            newvel = self.addtodir(-x * coef, -y * coef)
            self.addtopos(-x * overlap / dist, -y * overlap / dist)
            self.moveit()
            newvelotherobj = otherobj.addtodir(x * coef, y * coef)
            otherobj.moveit()
            return (1, newvel, newvelotherobj)
        return (0,)
    def madehole(self):
        """ checks if ball entered the hole """
        diff = (self.posreal[0] - Hole.pos[0], self.posreal[1] - Hole.pos[1])
        sqdist = diff[0]**2 + diff[1]**2
        # this checks whether d(b,h) < radius  (b = center of ball, h = center of hole)
        if sqdist < self.sqradius:
            return (self,1)# self is the object 
        return 0

class Stick(CPSprite):
    def __init__(self, pic, start):
        CPSprite.__init__(self)
        self.image = self.origpic = pic
        self.origsize = pic.get_rect()
        self.angle = 0
        self.start = start
        self.rect = self.posint = pic.get_rect().move(start)
        self.disp = [0,0]
        self.maxstrength = 80 # the maximum force to hit the ball
    def prepare(self, posmouse, posball):
        self.positmouse = posmouse
        dif = (posball[0] - posmouse[0], posball[1] - posmouse[1])
        dist = sqrt(dif[0]**2 + dif[1]**2)
        dif = (dif[0]/dist, dif[1]/dist)
        self.angle = acos(dif[0]) * 180 / 3.141593
        if dif[1] > 0:
            self.angle = 360 - self.angle
        self.image = self.pic = pygame.transform.rotate(self.origpic, self.angle)
        if dif[0]>0:
            self.disp[0] = -self.origsize.right*dif[0]
        else:
            self.disp[0] = 0
        if dif[1]>0:
            self.disp[1] = -self.origsize.right*dif[1]
        else:
            self.disp[1] = 0
        self.displacement = (posmouse[0]+self.disp[0],posmouse[1]+self.disp[1])
        self.rect = self.posint = self.pic.get_rect().move(self.displacement)
    def update(self, *args):
        pass
    def grow(self, scale):
        # make the stick grow to indicate strength
        sc = (float(scale)/self.maxstrength)**2 + 1
        self.image = pygame.transform.scale(self.pic, (int(self.posint.width * sc), int(self.posint.height * sc) ))
        self.rect = self.pic.get_rect().move(self.positmouse[0] + int(self.disp[0]*sc), self.positmouse[1] + int(self.disp[1]*sc) )
        Misc.group.refresh()
    def draw(self, screen):
        screen.blit(self.pic, self.posint)
        
        

class moving_sign(CPSprite):
    def __init__(self, txt, size, start, stop, step, fcol, scr, bck, era):
        pass
        CPSprite.__init__(self)
        self.image,self.size = font2surf(txt, size, fcol)
        self.rect = self.image.get_rect()
        self.set_movement(start, stop, step, 1, 0)
        self.add(Misc.movingsign)
        self.display_sprite()
        self.wait = max(abs(stop[0]-start[0]),abs(stop[1]-start[1]))
        while self.wait:
            self.wait -= step
            Misc.movingsign.refresh()
        self.remove(Misc.movingsign)
        if era:
            Misc.movingsign.refresh()
    def on_update(self, *args):
        self.moveit()
        pygame.time.wait(3)


class Game(Img):
    """  billiard.py , a billiards game. """
    def __init__(self, screen, backgr, backcol, basepath, libdir):
        self.screen = screen
        self.backgr = backgr
        self.basedir = libdir
        self.stop = 0
        self.score = 0
        self.level = 1 #No need, see self.gamelevels
        Hole.img = load_image(os.path.join(self.basedir,'BilliardData','hole.png'),0).convert()
        Hole.size = (Hole.img.get_width(), Hole.img.get_height())
        #Hole.pos = (570,244)
        self.buttonpressed = 0
        # these two are mandatory within childsplaytest.py
        self.gamelevels = (1,2,3)# these become the number of levels
        self.gameitems = (None,)# optional value to pass to each level
        self.maxscores = (100,200,300)# see _made_hole
        #self.maxscores = (2,4,6)# just for testing
        self.setup()
        #There are optimized sound/music functions in utils.py
        #playmusic = pygame.mixer.music 
        Sound.throw = os.path.join(self.basedir,'BilliardData','sndt.wav')
        Sound.hurra = os.path.join(self.basedir,'BilliardData','sndh.wav')
        Sound.great = os.path.join(self.basedir,'BilliardData','sndh.wav')
        # CPinit should only be called once to init the Spriteutils stuff
        # You must also call CPinit before anything else
        # Use CPGroup if you want a Spriteutils group
        Misc.actives = CPinit(self.screen, self.backgr)
        Misc.movingsign = CPGroup(self.screen, self.backgr)
        Misc.group = CPGroup(self.screen, self.backgr)
        bilgame = moving_sign(_("Billiard Game"), 80, (100,100), (100,400), 1, (8,0,120), self.screen, self.backgr, 1)
        
    def setup(self):
        pass
        self.backgr_wohole = load_image(os.path.join(self.basedir,'BilliardData','backgr.png'),0).convert()
        self.backgr.blit(self.backgr_wohole, (0,0))
        self.screen.blit(self.backgr_wohole, (0,0))
        pygame.display.update()
    
    def start(self,level,items):
        """ Put the ball(s) in place """
        # start is called by childsplay after the game is loaded and constructed
        self.level = level# make it part of this class so we can use it to test
        # in which level we are. See def _made_hole
        self.throws = 0
        Misc.actives.empty()
        Misc.group.empty()
        Misc.score = 0# see def loop and further
        objects = []
        self.screen.blit(self.backgr_wohole, (0,0)) # this is needed here, as CP calls the start (and not setup) proc. on a level change.
        x,y = (random.randrange(8,791-Hole.size[0],1),random.randrange(8,491-Hole.size[1],1))
        Hole.pos = (x,y)
        objects.append([Hole.pos,Hole.size])
        self.backgr.blit(self.backgr_wohole, (0,0))
        self.backgr.blit(Hole.img, Hole.pos)
        self.screen.blit(self.backgr,(0,0))
        pygame.display.update()
        img = load_image(os.path.join(self.basedir,'BilliardData','ball1.png'),1).convert()
        imgsize = (img.get_width(), img.get_height())
        x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
        while self.tooclose((x,y,imgsize), objects):
            x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
        objects.append([(x,y),imgsize])
        self.ball1 = Ball(img, (x,y),'blue')# the extra argument can be 'blue' or 'red'. this way we know what kind of ball it is.
        # use this if you just want to display a sprite
        self.ball1.display_sprite()
        Misc.actives.add(self.ball1)
        Misc.group.add(self.ball1)
        # Now it has level 2 repeated in level 3. Before it was (if level == 1), (if level > 1), (if level > 2).
        if level == 1:
            # set points to earn: just d(b1,h)/8 in this case
            self.pointstoadd = ( sqrt((self.ball1.posint[0] - Hole.pos[0])**2\
                            + (self.ball1.posint[1] - Hole.pos[1])**2) ) / 8
        if level == 2:
            img = load_image(os.path.join(self.basedir,'BilliardData','ball2.png'),1).convert()
            imgsize = (img.get_width(), img.get_height())
            x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
            while self.tooclose((x,y,imgsize), objects):
                x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
            self.ball2 = Ball(img, (x,y),'red')
            #self.screen.blit(img,(x,y))
            self.ball2.display_sprite()
            Misc.actives.add(self.ball2)
            Misc.group.add(self.ball2)
            # set points to earn: 0.2 * d(b2,h) + (2.4 + cos angle) * d(b2,b1) / 8
            # where b1,b2 = ball1,ball2, h = hole, d = distance, angle = angle at b2.
            diff1 = (Hole.pos[0] - self.ball2.posint[0], Hole.pos[1] - self.ball2.posint[1])
            diff2 = (self.ball1.posint[0] - self.ball2.posint[0], self.ball1.posint[1] - self.ball2.posint[1])
            diff1norm = sqrt(diff1[0]**2+diff1[1]**2)
            diff2norm = sqrt(diff2[0]**2+diff2[1]**2)
            self.pointstoadd = 0.2 * diff1norm + (diff1[0]*diff2[0]+diff1[1]*diff2[1]) / (6 * diff1norm) + diff2norm * 0.3
        if level == 3:
            img = load_image(os.path.join(self.basedir,'BilliardData','ball2.png'),1).convert()
            imgsize = (img.get_width(), img.get_height())
            x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
            while self.tooclose((x,y,imgsize), objects):
                x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
            objects.append([(x,y),imgsize])
            self.ball2 = Ball(img, (x,y),'red')
            x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
            while self.tooclose((x,y,imgsize), objects):
                x,y = (random.randrange(8,791-imgsize[0],1),random.randrange(8,491-imgsize[1],1))
            self.ball3 = Ball(img, (x,y),'red')
            self.ball2.display_sprite()
            self.ball3.display_sprite()
            Misc.actives.add((self.ball2,self.ball3))
            Misc.group.add((self.ball2,self.ball3))
            #points to earn: as in level 2  +  0.2 * d(b3,h) + (2.4 + cos angle) * d(b3,b1) / 8
            diff1 = (Hole.pos[0] - self.ball2.posint[0], Hole.pos[1] - self.ball2.posint[1])
            diff2 = (self.ball1.posint[0] - self.ball2.posint[0], self.ball1.posint[1] - self.ball2.posint[1])
            diff1norm = sqrt(diff1[0]**2+diff1[1]**2)
            diff2norm = sqrt(diff2[0]**2+diff2[1]**2)
            self.pointstoadd = 0.2 * diff1norm + (diff1[0]*diff2[0]+diff1[1]*diff2[1]) / (8 * diff1norm) + diff2norm * 0.3
            diff1 = (Hole.pos[0] - self.ball3.posint[0], Hole.pos[1] - self.ball3.posint[1])
            diff2 = (self.ball1.posint[0] - self.ball3.posint[0], self.ball1.posint[1] - self.ball3.posint[1])
            diff1norm = sqrt(diff1[0]**2+diff1[1]**2)
            diff2norm = sqrt(diff2[0]**2+diff2[1]**2)
            self.pointstoadd += 0.2 * diff1norm + (diff1[0]*diff2[0]+diff1[1]*diff2[1]) / (8 * diff1norm) + diff2norm * 0.3
        img = load_image(os.path.join(self.basedir,'BilliardData','stick.png'),1).convert()
        self.stick = Stick(img, (200,120))
        
    def tooclose(self, ob1, obs):
        for i in obs:
            if (((ob1[0]-i[0][0])**2+(ob1[1]-i[0][1])**2) < (ob1[2][0]+i[1][0])**2/4) :
                return 1
        return 0
        
    def movethings(self):
        """ moves everything. Deletes from actives if stop. """
        for o in Misc.actives.sprites():
            keepit = o.moveit()
            # remove it from actives if it has stopped.
            if not keepit:
                Misc.actives.remove(o)
        # check all possible collisions
        for n1 in range(len(Misc.group.sprites())):
            o1 = Misc.group.sprites()[n1]
            for o2 in Misc.group.sprites()[n1:]:
                if o1 != o2:
                    newdirs = o1.checkcollision(o2)
                    if newdirs[0]:
                        if newdirs[1] == 0 and o1 in Misc.actives.sprites():
                            Misc.actives.remove(o1)
                        elif newdirs[1] and o1 not in Misc.actives.sprites():
                            Misc.actives.add(o1)
                        if newdirs[2] and o2 not in Misc.actives.sprites():
                            Misc.actives.add(o2)
                        elif newdirs[2] == 0 and o2 in Misc.actives.sprites():
                            Misc.actives.remove(o2)
    
    def _made_hole(self,obj):
        # This replaces _level_1,_level_2 and _level_3
        if self.level > 1 and str(obj) == 'blue':
            return# we pot a red blue ball in the levels > 1
        obj.remove(Misc.group)# remove the ball from the group
        obj.remove_sprite()
        load_music(Sound.hurra).play()
        if (len(Misc.group) == (self.level > 1)): # true if there are no balls in level 1 or there's in levels 2 and 3
            pygame.time.wait(1000)
            s = int(self.pointstoadd / self.throws)
            ## Consider this a placeholder, it doesn't work as it should
            ChildsplayGoodies.scorething(s)# Update the score display
            self.score += s
            if self.score > self.maxscores[self.level-1]:# see, Game -> gameitems,gamelevels
                self.stop = -1#signal to childsplay that this level is ended. See return val of def loop
                # when the level is ended childsplay will call the start method of the game again and then the loop.
                if self.level == 3:
                    self.stop = 1
                    Misc.score = self.score
                    self.screen.blit(self.backgr, (0,0))
                    moving_sign(_("You won!!"), 80, (100,100), (100,300), 1, (8,0,120), self.screen, self.backgr, 0)
                    txt,sz = font2surf(str(self.score)+'  '+_("points"), 80, (8, 0, 120))
                    self.screen.blit(txt, (100,200))
                    pygame.display.update()
                    for i in range(0,3):
                        load_music(Sound.hurra).play()
                        pygame.time.wait(1000)
            else:
                self.start(self.level,None)

    def loop(self,events):
        # More info about these stop and score stuff below
        self.stop = 0
        if not self.buttonpressed:
            for event in events:
                if event.type is MOUSEBUTTONDOWN:
                    #if self.ball1 in Misc.actives.sprites():
                    if len(Misc.actives):
                        continue
                    if event.button >= 2:
                        if self.ball1.callback(event.pos, event.button):
                            # put stick in position to see the direction it will have
                            self.stick.prepare(event.pos, (self.ball1.posreal[0]+self.ball1.size[0]/2, self.ball1.posreal[1]+self.ball1.size[1]/2))
                            Misc.group.add(self.stick)
                            Misc.group.refresh()
                    else:
                        if self.ball1.callback(event.pos, event.button):
                            # there will be a kick. Begin counting the time buttonmouse is pressed.
                            self.buttonpressed = 1
                            self.timepressed = 0
                            self.stick.prepare(event.pos, (self.ball1.posreal[0]+self.ball1.size[0]/2, self.ball1.posreal[1]+self.ball1.size[1]/2))
                            Misc.group.add(self.stick)
                            Misc.group.refresh()
                if event.type is KEYDOWN and (event.key == 113):
                    # My mind is going down...
                    #self.stop = 1
                    self.start(self.level,None)
                if event.type is MOUSEBUTTONUP:
                    Misc.group.remove(self.stick)
                    Misc.group.refresh()
        else:
            if len(events):
                for event in events:
                    if event.type is MOUSEBUTTONUP and event.button == 1:
                        # stick was released. It's time to roll.
                        self.stick.remove(Misc.group)
                        self.buttonpressed = 0
                        #Misc.group.refresh()
                        Misc.actives.add(self.ball1)
                        strength = self.timepressed / 2
                        self.ball1.direc = (self.ball1.direc[0] * strength, self.ball1.direc[1] * strength)
                        self.throws += 1
                        load_music(Sound.throw).play()
            else:
                # strength is going up
                if self.timepressed < self.stick.maxstrength:
                    self.timepressed += 1
                    self.stick.grow(self.timepressed)
        if Misc.actives.sprites():
            #pygame.time.wait(10)
            self.movethings()
            # Call refresh with a true value to let it call callback and update methods.
            # See the game fallingletters for a simple implementation of this in action.
            v = Misc.group.refresh(1)# The ball returns 1 when it goes into the hole 
            # the ball also removes it self from the grou. see the ball class

            # check the return val from the on_update method
            # for the format see the reference and the ball class
            if v:
                #print v # just as info to see how the return val looks like :-)
                if v[0][1][1] == 1:
                    self._made_hole(v[0][1][0])
        return self.stop, 0
        # When this loop return childsplay checks for the stop status.
        # Childsplay also checks to see if there's a score value, if so
        # it adds this value to a total.
        # IMPORTANT, childsplay assumes that you reset the score value.
        # If not childsplay keeps adding them up :-)
        
    def helptitle(self):
        """This will become the title of the game used to display"""
        return _("Billiard")
    
    def __str__(self):
        """Must return the original, not translated, title of this game.
        It's needed by the high score class of childsplay."""        
        return "Billiard"
    
    def help(self):
        #The length of the strings doesn't matter, childsplay takes care of that :-)
        #i removed the "Billiards game" string, it's the same as the title.
        text = [_("The aim of the game:"),
        " ",
        _("You have to make the blue ball enter the hole (in level 1)"),
        _("and the red ones in levels 2 and 3."),
        _("Use the right mousebutton to aim and the left button to hit the ball."),
        _("The longer you hold the left button the harder it will hit the ball."),
        _("The fewer hits you need to get the ball in the hole, the more points you get."),
        " ",
        _("Difficulty : 4-7 years"),
        " ",
        _("Number of levels : 3")]

        return text
