[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[pygame] pong/tele-tennis with sound!



A two-player game small enough to squeeze onto a mailing list!
With sound effects!

My brain didn't spot the sprite collision-detection stuff
though ... Oh, well ...

cheers,
John.
"""
A clone of the game of pong or tele-tennis.
Requires Python and PyGame:
    http://www.python.org/ and http://pygame.seul.org/

Try 'python pypong.py --help' for instructions.

This code is in the public domain.
[Tested on Win98 / Win2K / GNU/Linux (SuSE)]

Version:  0.0.1
Author :  John Popplewell
Email  :  john@johnnypops.demon.co.uk
Web    :  http://www.johnnypops.demon.co.uk/
"""

import os, sys, getopt, math, random
try:
    import Numeric as N
except:
    print "This game requires the Numeric module."
    sys.exit()
try:
    import pygame
except:
    print "This game requires the PyGame module."
    sys.exit()

from pygame.locals import *

file_name = "pypong"
demo_name = "pyPong"

if not pygame.font:
    print 'Warning, fonts disabled'
if not pygame.mixer:
    print 'Warning, sound disabled'


LEFT_PLAYER_UP     = K_q
LEFT_PLAYER_DOWN   = K_a
LEFT_PLAYER_SERVE  = K_s
RIGHT_PLAYER_UP    = K_p
RIGHT_PLAYER_DOWN  = K_l
RIGHT_PLAYER_SERVE = K_k

SCREEN_WIDTH     = 640
SCREEN_HEIGHT    = 480

BAT_WIDTH        = 12
BAT_HEIGHT       = 80
BAT_INDENT       = 10
BAT_VELOCITY     = 256.0
BAT_ACCELERATION = 300.0
BAT_SPIN_FACTOR  = -0.333
BAT_CURVATURE    = -8.0

BORDER_THICKNESS = 2

BALL_SIZE        = 12
BALL_VELOCITY    = 360.0
MAX_BALL_VELOCITY= 460.0
BALL_WOBBLE      = BALL_VELOCITY/10.0

SCORE_INDENT     = 10
SCORE_POINT_SIZE = 36

TICK_PER_SECOND  = 40

SND_STEREO          = 0
SND_BITS_PER_SAMPLE = 16
SND_SAMPLES_PER_SEC = 22050

game_font = None
all_sprites = None
visible_sprites = None

def make_tone(frequency, duration, samples_per_sec, bits_per_sample):
    samples_per_cycle = int(math.ceil(samples_per_sec/frequency))
    total_samples = samples_per_cycle*int(frequency*duration)
    samples = N.zeros(total_samples, N.Int16)
    amplitude = ((2**bits_per_sample)/2)-1
    k = 2.0*math.pi/samples_per_cycle
    for i in range(total_samples):
        samples[i] = int(amplitude*math.sin(k*(i%samples_per_cycle)))
    res = pygame.sndarray.make_sound(samples)
    res.set_volume(1.0)
    return res

def sign(i):
    if i < 0: return -1
    if i > 0: return  1
    return 0


class ScoreBoard(pygame.sprite.Sprite):
    LEFT, CENTER, RIGHT = range(3)

    def __init__(self, x, y, align, colour ):
        pygame.sprite.Sprite.__init__(self)
        self.x = x
        self.y = y
        self.align = align
        self.colour = colour
        self.score = 0
        self.update_score(0)
        visible_sprites.add(self)

    def update_score(self, delta):
        self.score += delta
        if not game_font:
            return
        self.image = game_font.render("%d"%self.score, 1, self.colour)
        self.rect = self.image.get_rect()
        self.rect.top = self.y
        if self.align == ScoreBoard.LEFT:
            self.rect.left = self.x
        elif self.align == ScoreBoard.CENTER:
            self.rect.centerx = self.x
        elif self.align == ScoreBoard.RIGHT:
            self.rect.right = self.x


class Bat(pygame.sprite.Sprite):
    def __init__(self, x, height_range, colour):
        pygame.sprite.Sprite.__init__(self)
        self.height_range = height_range
        self.rect = pygame.Rect((0,0,BAT_WIDTH,BAT_HEIGHT)) 
        self.image = pygame.Surface((BAT_WIDTH,BAT_HEIGHT))
        self.image.fill(colour) 
        self.p_x = float(x)
        self.p_y = float((self.height_range-self.rect.height)/2)
        self.dir = 0
        self.v_y = 0.0
        self.rect.move_ip(round(self.p_x),round(self.p_y))

    def update(self,dT):
        self.v_y += sign(self.v_y)*BAT_ACCELERATION*dT
        self.p_y += self.v_y*dT
        if self.p_y <= 0.0:
            self.p_y = 0.0
            self.v_y = 0.0
        if self.p_y+BAT_HEIGHT >= SCREEN_HEIGHT:
            self.p_y = SCREEN_HEIGHT-BAT_HEIGHT
            self.v_y = 0.0

        self.rect.top = round(self.p_y)

    def move(self,dir):
        self.dir = dir
        self.v_y = self.dir*BAT_VELOCITY
        
    def stop(self,dir):
        if dir == self.dir:
            self.move(0)


class Player:
    properties = (
                    (BAT_INDENT , 0.25),
                    (SCREEN_WIDTH-BAT_INDENT-BAT_WIDTH, 0.75),
                 )
    controls   = (
                    (LEFT_PLAYER_UP,  LEFT_PLAYER_DOWN),
                    (RIGHT_PLAYER_UP, RIGHT_PLAYER_DOWN),
                 )

    def __init__(self, player_idx, colour, miss_sfx):
        self.props = Player.properties[player_idx]
        self.controls = Player.controls[player_idx]
        self.bat = Bat(self.props[0], SCREEN_HEIGHT, colour)
        self.score = ScoreBoard(self.props[1]*SCREEN_WIDTH, SCORE_INDENT, ScoreBoard.RIGHT, colour)
        if pygame.mixer:
            self.chan = pygame.mixer.Channel(1)
        else: self.chan = None
        self.miss_sfx = miss_sfx

    def inc_score(self):
        self.score.update_score(1)
        if self.chan: self.chan.play(self.miss_sfx, 0, 200)

    def key_down(self, key):
        if key == self.controls[0]:
            self.bat.move(-1)
            return 1
        if key == self.controls[1]:
            self.bat.move( 1)
            return 1
        return 0

    def key_up(self, key):
        if key == self.controls[0]:
            self.bat.stop(-1)
            return 1
        if key == self.controls[1]:
            self.bat.stop( 1)
            return 1
        return 0


class Ball(pygame.sprite.Sprite):
    READY, PLAYING = range(2)

    def __init__(self, x, y, colour, bat_sfx, wall_sfx, players, out_of_play):
        pygame.sprite.Sprite.__init__(self)
        self.rect = pygame.Rect((0,0,BALL_SIZE,BALL_SIZE)) 
        self.image = pygame.Surface((BALL_SIZE,BALL_SIZE))
        self.image.fill(colour)
        self.visible = 0
        self.gen = random.Random()
        self.p_x = float(x)
        self.p_y = float(y)
        self.v_x = BALL_VELOCITY
        self.v_y = self.random_velocity(BALL_VELOCITY)
        self.bat_sfx = bat_sfx
        self.wall_sfx = wall_sfx
        if pygame.mixer:
            self.chan = pygame.mixer.Channel(0)
        else: self.chan = None
        self.players = players
        self.out_of_play = out_of_play
        self.status = Ball.READY
        self.set_rect()

    def set_rect(self):
        self.rect.left = round(self.p_x)
        self.rect.top = round(self.p_y)

    def get_bat(self, idx):
        return self.players[idx].bat

    def update(self,dT):
        if self.status == Ball.READY:
            return

        self.p_x += self.v_x*dT
        self.p_y += self.v_y*dT
        if self.p_y <= 0.0:
            self.p_y = 0.0
            self.v_y = -self.v_y
            if self.visible:
                if self.chan: self.chan.play(self.wall_sfx, 0, 150)
        elif self.p_y+BALL_SIZE >= SCREEN_HEIGHT:
            self.p_y = SCREEN_HEIGHT-BALL_SIZE
            self.v_y = -self.v_y
            if self.visible:
                if self.chan: self.chan.play(self.wall_sfx, 0, 150)
        if self.p_x <= 0.0:
            self.show(0)
            self.status = Ball.READY
            self.out_of_play(1)
        elif self.p_x+BALL_SIZE >= SCREEN_WIDTH:
            self.show(0)
            self.status = Ball.READY
            self.out_of_play(0)
        elif self.collide_player(0):
            pass
        elif self.collide_player(1):
            pass
        self.set_rect()

    def collide_player(self, idx):
        if self.approaching_bat(idx) and self.rect.colliderect(self.get_bat(idx).rect):
            self.align_ball_on_bat(idx)
            self.v_x = -self.v_x
            self.v_y += self.get_bat(idx).v_y*BAT_SPIN_FACTOR+self.random_velocity(BALL_WOBBLE)
            self.v_y += (self.get_bat(idx).rect.centery-self.rect.centery)*BAT_CURVATURE
            self.clamp_velocity()
            if self.visible:
                if self.chan: self.chan.play(self.bat_sfx, 0, 120)
            return 1
        return 0

    def approaching_bat(self, idx):
        return ((self.v_x < 0), (self.v_x > 0))[idx]

    def align_ball_on_bat(self, idx):
        self.p_x = (self.get_bat(idx).rect.right, self.get_bat(idx).rect.left-BALL_SIZE)[idx]

    def random_velocity(self, amplitude):
        return 2.0*amplitude*self.gen.random()-amplitude

    def clamp_velocity(self):
        self.v_y = max(min(MAX_BALL_VELOCITY,self.v_y),-MAX_BALL_VELOCITY)

    def serve(self, idx):
        if self.status != Ball.READY:
            return
        self.p_x = (0.0, SCREEN_WIDTH-BALL_SIZE)[idx]
        self.p_y = (self.get_bat(idx).rect.centery)-BALL_SIZE/2
        self.v_x = (1.0,-1.0)[idx]*BALL_VELOCITY
        self.v_y = self.random_velocity(BALL_VELOCITY)
        self.status = Ball.PLAYING
        self.set_rect()
        self.show(1)

    def show(self, visible):
        if self.visible == visible:
            return
        self.visible = visible
        if visible:
            visible_sprites.add(self)
        else:
            visible_sprites.remove(self)


class Game:
    def __init__(self,left_colour,right_colour,ball_colour):
        if pygame.mixer:
            miss_sound = make_tone(131.25, 1.0, SND_SAMPLES_PER_SEC, SND_BITS_PER_SAMPLE)
            wall_sound = make_tone(262.5, 1.0, SND_SAMPLES_PER_SEC, SND_BITS_PER_SAMPLE)
            bat_sound = make_tone(525.0, 1.0, SND_SAMPLES_PER_SEC, SND_BITS_PER_SAMPLE)
        else:
            miss_sound = None
            wall_sound = None
            bat_sound = None

        self.players = ( Player(0, left_colour, miss_sound), Player(1, right_colour, miss_sound) )
        self.ball = Ball(0, 0, ball_colour, bat_sound, wall_sound, self.players, self.out_of_play)

        all_sprites.add((self.players[0].bat, self.players[1].bat, self.ball))
        visible_sprites.add((self.players[0].bat, self.players[1].bat))

        self.service = -1

    def out_of_play(self, player_idx):
        self.service = player_idx
        self.players[player_idx].inc_score()

    def serve(self, player_idx):
        if self.service == -1:
            self.service = player_idx
        if self.service != player_idx:
            return
        self.ball.serve(player_idx)

    def handle_event(self, event):
        if event.type is KEYDOWN:
            if event.key is K_ESCAPE:
                return
            if self.players[0].key_down(event.key):
                pass
            elif self.players[1].key_down(event.key):
                pass
            elif event.key == LEFT_PLAYER_SERVE:
                self.serve(0)
            elif event.key == RIGHT_PLAYER_SERVE:
                self.serve(1)
        elif event.type is KEYUP:
            if self.players[0].key_up(event.key):
                pass
            elif self.players[1].key_up(event.key):
                pass


def Usage():
    print """python %s.py
    [-f|--fullscreen]     fullscreen display (640x480)
    [-c|--colour|--color] colour paddles and ball
    [-q|--quiet]          no sound
    [-h|--help]           this text

    Left player controls: Right player controls:
        Q - UP                  P - UP
        A - DOWN                L - DOWN
        S - SERVE               K - SERVE

      ESC - Quit

A clone of the game of pong or tele-tennis.

The players decide who serves first. Service goes to the winner 
of a rally. Players should agree to end the match on a first to 
score N basis e.g. first player to score 5 points is the winner. 

Have Fun!
This code is in the public domain.

Version:  0.0.1
Author :  John Popplewell
Email  :  john@johnnypops.demon.co.uk
Web    :  http://www.johnnypops.demon.co.uk/
"""%(file_name,)


def main():
    fullscreen = 0
    monochrome = 1
    try:
        opts, args = getopt.getopt(sys.argv[1:], "fcqh", ["fullscreen","colour","color","quiet","help"])
        for o, a in opts:
            if o in ("-f", "--fullscreen"):
                fullscreen = 1
            if o in ("-c", "--colour", "--color"):
                monochrome = 0
            if o in ("-q", "--quiet"):
                pygame.mixer = None
            if o in ("-h", "--help"):
                Usage()
                sys.exit()
    except getopt.GetoptError:
        Usage()
        sys.exit()

    if pygame.mixer:
        pygame.mixer.pre_init(SND_SAMPLES_PER_SEC, -SND_BITS_PER_SAMPLE, SND_STEREO) 

    pygame.init()
    pygame.mouse.set_visible(0)
    if fullscreen:
        #vidflags = HWSURFACE|DOUBLEBUF|FULLSCREEN
        vidflags = FULLSCREEN
    else:
        vidflags = 0

    if monochrome:
        BACKGROUND_COLOUR= (0,0,0)
        BORDER_COLOUR    = (255,255,255)
        BALL_COLOUR      = (255,255,255)
        LEFT_BAT_COLOUR  = (255,255,255)
        RIGHT_BAT_COLOUR = (255,255,255)
    else:
        BACKGROUND_COLOUR= (0,0,0)
        BORDER_COLOUR    = (0,0,255)
        BALL_COLOUR      = (0,255,255)
        LEFT_BAT_COLOUR  = (255,0,0)
        RIGHT_BAT_COLOUR = (0,255,0)

    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vidflags )
    pygame.mouse.set_visible(0)

    pygame.display.set_caption(demo_name)

    background = pygame.Surface(screen.get_size())
    background = background.convert()
    background.fill(BACKGROUND_COLOUR)
    if fullscreen:
        rc = (0, 0, SCREEN_WIDTH, BORDER_THICKNESS)
        pygame.draw.rect(background, BORDER_COLOUR, rc)
        rc = (0, SCREEN_HEIGHT-BORDER_THICKNESS, SCREEN_WIDTH, BORDER_THICKNESS)
        pygame.draw.rect(background, BORDER_COLOUR, rc)
    
    screen.blit(background, (0, 0))
    pygame.display.flip()

    global game_font, all_sprites, visible_sprites

    if pygame.font:
        game_font = pygame.font.Font(None, SCORE_POINT_SIZE)
    all_sprites = pygame.sprite.Group()
    visible_sprites = pygame.sprite.RenderUpdates()

    the_game = Game(LEFT_BAT_COLOUR, RIGHT_BAT_COLOUR, BALL_COLOUR)

    clock = pygame.time.Clock()
    clock.tick(TICK_PER_SECOND)

    quit = 0
    while not quit:
        for event in pygame.event.get():
            if event.type is QUIT:
                quit = 1
            if event.type is KEYDOWN and event.key is K_ESCAPE:
                quit = 1
            the_game.handle_event(event)

        visible_sprites.clear(screen, background)
        all_sprites.update(clock.tick(TICK_PER_SECOND)/1000.0)
        dirty = visible_sprites.draw(screen)
        pygame.display.update(dirty)
    pygame.quit()


if __name__ == '__main__':
    main()

# vim: et noai tw=0 ts=4