# /*************************************************************************** # * 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