""" Simplified Pong! Attributions under CC: http://creativecommons.org/licenses/by/3.0/ Sounds: PongBlipF4.wav http://www.freesound.org/people/NoiseCollector/sounds/4359/ Error.wav http://www.freesound.org/people/Autistic%20Lucario/sounds/142608/ """ import math import simplegui import random #Globals # Room dimensions room_width = 600 room_height = 400 top_margin = 80 room_center = ( room_width/2, (room_height + top_margin)/2 ) # Ball parameters ball_radius = 8 # Paddle parameters player1_speed = 8 player2_speed = player1_speed paddle_length = 80 paddle_width = 8 # Paddle Movement sensitivity UP = (0, -1) # move paddle in up/negative Y direction DOWN = (0, 1) # move paddle in down/positive Y direction # Single or Two Player player2_auto = False """ Classes """ ## Geometric Primitives # Rectangle class Rect(object): def __init__(self, center, width, height, color="White"): self.width = width self.height = height self.center = center self.color = color def getWidth(self): return self.width def getHeight(self): return self.height def getCenter(self): return self.center def getTopLeft(self): """ Calculate topLeft based on center coordinates. Useful when moving the rect by changing center coordinates """ center = self.center width = self.width height = self.height return ((center[0] - width/2), (center[1] - height/2)) def pointList(self): topLeft = self.getTopLeft() return ([topLeft, \ (topLeft[0] + self.width, topLeft[1]), \ (topLeft[0] + self.width, topLeft[1] + self.height), \ (topLeft[0], topLeft[1] + self.height)]) def intersection(self, point): """ Determine if a given point is inside or outside the rectangle point: tuple returns True if point is ON the edges or INSIDE the shape else False """ topLeft = self.getTopLeft() xIntersect = point[0] >= topLeft[0] \ and point[0] <= topLeft[0] + self.width yIntersect = point[1] >= topLeft[1] \ and point[1] <= topLeft[1] + self.height return xIntersect and yIntersect def draw(self, canvas): canvas.draw_polygon(self.pointList(), 1, self.color, self.color) # Circle class Circle(object): def __init__(self, center, radius, color="White"): self.center = center self.radius = radius self.color = color def getCenter(self): return self.center def getRadius(self): return self.radius def distanceToCenter(self, point): """ Distance between a point somewhere in space, and the center of this circle """ return math.sqrt( (point[0] - self.center[0])**2 + (point[1] - self.center[1])**2 ) def getBoundingBox(self): return Rect(self.getCenter(), self.getRadius()*2, self.getRadius()*2) def intersection(self, point): """ Determine if a given point is inside or outside the circle point: tuple returns True if point is ON the edges or INSIDE the shape else False """ distance = self.distanceToCenter(point) return self.radius >= distance def draw(self, canvas): canvas.draw_circle(self.center, self.radius, 1, self.color, self.color) # Classes: Vectors (cartesian) class Vector(object): def __init__(self, coordinates): self.coordinates = coordinates def getCoordinates(self): # Returns Cartesian coordinates of this vector return self.coordinates def getMagnitude(self): # Returns magnitude of this vector x = self.coordinates[0] y = self.coordinates[1] return math.sqrt(x**2 + y**2) def dot(self, v2): """ Dot product of this and v2 Returns a scalar value """ x = self.coordinates[0] y = self.coordinates[1] v2_coord = v2.getCoordinates() result = (x * v2_coord[0]) + (y * v2_coord[1]) return result def __add__(self, v2): """ Add this vector to another vector v2 Returns tuple of resultant vector """ x = self.coordinates[0] y = self.coordinates[1] v2_coord = v2.getCoordinates() result = (x + v2_coord[0], y + v2_coord[1]) return Vector(result) def __sub__(self, v2): """ Subtract v2 from this vector Returns tuple of resultant vector """ x = self.coordinates[0] y = self.coordinates[1] v2_coord = v2.getCoordinates() result = (x - v2_coord[0], y - v2_coord[1]) return Vector(result) def normalize(self): """ Returns the normalized coordinates of this vector """ magnitude = self.getMagnitude() x = self.coordinates[0] y = self.coordinates[1] result = (x/magnitude, y/magnitude) return Vector(result) def scalarMul(self, C): """ Scalar multiplication of a vector Returns tuple containing coordinates multiplied by the scalar """ x = self.coordinates[0] y = self.coordinates[1] result = (C*x, C*y) return Vector(result) def proj(self, v2): """ Project this onto v2. v2 should be the surface normal of a wall or other object """ n = v2.normalize() return n.scalarMul(self.dot(n)) def reflectionVector(self, v2): """ http://math.stackexchange.com/questions/13261/how-to-get-a-reflection-vector Coupe de grace; Reflects this according to the law of reflection v2 is the surface normal of reflecting surface Returns a tuple containing coordinates of reflected vector """ v1_projected_v2 = (self.proj(v2)).scalarMul(2).getCoordinates() x = self.coordinates[0] - v1_projected_v2[0] y = self.coordinates[1] - v1_projected_v2[1] return Vector((x, y)) ## Classes: Game Elements # Ball class Ball(Circle): """ Define a ball object which is a circle with additional properties speed: position increment in pixels. Position will change with every call to move() direction: angle measured from positive x axis """ def __init__(self, center, radius, velocity): Circle.__init__(self, center, radius, "Yellow") self.velocity = velocity def getSpeed(self): return self.velocity.getMagnitude() def getDirection(self): return self.velocity.normalize().getCoordinates() def setPosition(self, center): self.center = center def getVelocity(self): return self.velocity def setVelocity(self, direction): self.velocity = Vector(direction) def move(self): c = self.getCenter() speed = self.getSpeed() d = self.getDirection() new_pos = [(c[0] + speed * d[0]), (c[1] + speed * d[1])] if new_pos[0] < 0: new_pos[0] = 0 elif new_pos[0] > room_width - paddle_width - 1: new_pos[0] = room_width - paddle_width - 1 if new_pos[1] < (top_margin + paddle_width)/2: new_pos[1] = (top_margin + paddle_width)/2 elif new_pos[1] > (top_margin + paddle_width)/2 + room_height: new_pos[1] = room_height self.setPosition( tuple(new_pos) ) def reflect(self, normal): """ The velocity vector is reflected according to the law of reflection, upon collision with a surface """ self.velocity = self.velocity.reflectionVector(normal) # Paddle class Paddle(Rect): """ Define a paddle object which is a Rect with additional properties speed: position increment in pixels. Position will change with every call to move() direction: either down or up """ def __init__(self, center, width, height, color, velocity, surfaceNormal): Rect.__init__(self, center, width, height, color) self.normal = surfaceNormal self.velocity = velocity def getSpeed(self): return self.velocity.getMagnitude() def getDirection(self): return self.velocity.normalize().getCoordinates() def getSurfaceNormal(self): return self.normal def setPosition(self, center): self.center = center def setVelocity(self, vel): self.velocity = Vector(vel) def move(self): speed = self.getSpeed() d = self.getDirection() newy = self.center[1] + speed * d[1] self.center = (self.center[0], newy) # Wall/Boundaries class Wall(Rect): """ Define a paddle object which is a Rect with additional properties speed: position increment in pixels. Position will change with every call to move() direction: either down or up """ def __init__(self, center, width, height, surfaceNormal): Rect.__init__(self, center, width, height) self.surfaceNormal = surfaceNormal def getSurfaceNormal(self): # Return the Surface Normal Vector return self.surfaceNormal """ Helper functions """ # Random Velocity def genRandomVelocity(sgn=0): if sgn != 0: # If sign is specified, only generate Y randomly x = sgn * random.randrange(120, 240) y = random.randrange(-1,2,2) * random.randrange(60, 180) return ((x/60, y/60)) else: # Sign is not specified, both components are random x = random.randrange(-1,2,2) * random.randrange(120, 240) y = random.randrange(-1,2,2) * random.randrange(60, 180) return ((x/60, y/60)) # Determine collisions def intersectingShapes(shape1, shape2): """ Takes 2 shapes and determines if there is any overlap returns: True if shape1, and shape2 overlap """ # Rect and Rect def overlappingRectangles(shape1, shape2): """ If rectangles are of different sizes, need to check if vertices of smaller rectangle are inside the larger one. """ vertices_shape1 = shape1.pointList() vertices_shape2 = shape2.pointList() for v1 in vertices_shape1: if shape2.intersection(v1): return True else: for v2 in vertices_shape2: if shape1.intersection(v2): return True else: return False # Circle and Rectangle def overlappingCircleandRect(shape1, shape2): if isinstance(shape1, Circle): c = shape1 r = shape2 else: c = shape2 r = shape1 boundingBox = c.getBoundingBox() vertices = r.pointList() for vertex in vertices: if c.intersection(vertex): return True else: return overlappingRectangles(r, boundingBox) if isinstance(shape1, Rect) and isinstance(shape2, Rect): return overlappingRectangles(shape1, shape2) elif isinstance(shape1, Circle) and isinstance(shape2, Rect) \ or isinstance(shape2, Circle) and isinstance(shape1, Rect): return overlappingCircleandRect(shape1, shape2) """ Initialize and Load Game """ def game_init(): """ Loads the game environment """ # Game elements global pong_ball, top_wall, bottom_wall, player1_paddle, player2_paddle, roomRect # Controls, scoring global keyboard, player1_score, player2_score, match_point # Flags global start_game, player1_out, player2_out # Game Assets global ball_hit_sound, ball_out_sound # Game Elements # Ball pong_ball = Ball( room_center, ball_radius, Vector(genRandomVelocity()) ) # Walls top_wall_y = top_margin/2 bottom_wall_y = top_wall_y + room_height + paddle_width top_wall = Wall( (room_width/2, top_wall_y), room_width, paddle_width, Vector((0, 1)) ) bottom_wall = Wall( (room_width/2, bottom_wall_y), room_width, paddle_width, Vector((0, -1)) ) # print (bottom_wall.getCenter()[1] - paddle_width/2) - (top_wall.getCenter()[1] + paddle_width/2) # should be == room_width # Paddles paddles_y = room_center[1] player1_vel = Vector(DOWN).scalarMul(player1_speed) player2_vel = Vector(DOWN).scalarMul(player2_speed) player1_paddle = Paddle( (paddle_width/2, paddles_y), paddle_width, paddle_length, "Red", player1_vel, Vector((1, 0)) ) player2_paddle = Paddle( (room_width - paddle_width/2 - 1, paddles_y), paddle_width, paddle_length, "Blue", player2_vel, Vector((-1, 0)) ) # Canvas (Game map/area) roomRect = Rect(room_center, room_width, room_height + top_margin) # User Control keyboard = {'up' : False, 'down' : False, 'w' : False, 's' : False, } # Scoring player1_score = 0 player2_score = 0 match_point = 7 # Flags start_game = False player1_out = False player2_out = False # Game Assets # Sounds ball_hit_sound = simplegui.load_sound("http://www.freesound.org/people/NoiseCollector/sounds/4359/download/4359__noisecollector__pongblipf4.wav") ball_out_sound = simplegui.load_sound("http://www.freesound.org/people/Autistic%20Lucario/sounds/142608/download/142608__autistic-lucario__error.wav") game_init() """ Movement and animation functions """ """ Ball Movements """ # Ball out of bounds def ball_outOfBounds(ball): global player1_out, player2_out c = ball.getCenter() r = ball.getRadius() d = ball.getDirection() p1 = player1_paddle.getCenter() p2 = player2_paddle.getCenter() if d[0] < 0: # Ball out of bounds on left side of screen if (c[1] + r <= p1[1] - player1_paddle.getHeight()/2)\ or (c[1] - r >= p1[1] + player1_paddle.getHeight()/2): ball_out = (c[0] - r <= paddle_width)\ or (c[0] + r >= room_width - paddle_width) if ball_out: player1_out = True return ball_out elif d[0] > 0: # Ball out of bounds on right side of screen if (c[1] + r <= p2[1] - player1_paddle.getHeight()/2)\ or (c[1] - r >= p2[1] + player1_paddle.getHeight()/2): ball_out = (c[0] - r <= paddle_width)\ or (c[0] + r >= room_width - paddle_width) if ball_out: player2_out = True return ball_out # Reset Ball Position def ball_reset(): """ Reset the ball, when it goes out of bounds """ global start_game, player1_out, player2_out start_game = player1_out = player2_out = False ball_out_sound.play() reset_timer.start() # Ball Reset timer def reset_pause(): global start_game start_game = True pong_ball.setPosition(room_center) reset_timer.stop() # Buffer zone around surfaces before smartly testing for collisions def ball_buffer(): """ Returns the distance between center of ball and center of paddle one timestep before imminent collision. """ return (paddle_width/2 + pong_ball.getSpeed() + ball_radius) # Smartly determine if ball is in dangerzone (dz) and # collision is imminent (after one additional timestep) dz_left = player1_paddle.getCenter()[0] + ball_buffer() dz_right = player2_paddle.getCenter()[0] - ball_buffer() dz_top = top_wall.getCenter()[1] + ball_buffer() dz_bottom = bottom_wall.getCenter()[1] - ball_buffer() def ball_collisions(): """ Since the game has only one moving object, with 4 fixed surfaces, and a room of known size significant efficiency gains can be achieved by not checking for collisions on every single draw callback. This function will smartly check for collisions based on distance between surfaces and ball """ pong_ball_center = pong_ball.getCenter() pong_ball_dir = pong_ball.getDirection() # Left paddle (player 1) if pong_ball_center[0] <= dz_left and pong_ball_dir[0] < 0: if intersectingShapes(player1_paddle, pong_ball): ball_hit_sound.play() pong_ball.reflect(player1_paddle.getSurfaceNormal()) pong_ball.setVelocity(pong_ball.getVelocity().scalarMul(1.1).getCoordinates()) # Right paddle (player 2) elif pong_ball_center[0] >= dz_right and pong_ball_dir[0] > 0: if intersectingShapes(player2_paddle, pong_ball): ball_hit_sound.play() pong_ball.reflect(player2_paddle.getSurfaceNormal()) pong_ball.setVelocity(pong_ball.getVelocity().scalarMul(1.1).getCoordinates()) # Top wall if pong_ball_center[1] <= dz_top and pong_ball_dir[1] < 0: if intersectingShapes(top_wall, pong_ball): ball_hit_sound.play() pong_ball.reflect(top_wall.getSurfaceNormal()) # Bottom wall elif pong_ball_center[1] >= dz_bottom and pong_ball_dir[1] > 0: if intersectingShapes(bottom_wall, pong_ball): ball_hit_sound.play() pong_ball.reflect(bottom_wall.getSurfaceNormal()) """ Scoring """ def calculateScores(): global player1_score, player2_score # Updates scores. if player2_out: player1_score += 1 elif player1_out: player2_score += 1 def isGameOver(): return player1_score == match_point\ or player2_score == match_point """ User Controls: Paddle Movements """ # Determine if paddle is able to move def paddle_validMove(paddle): """ Move the paddle if it won't collide with the walls """ speed = paddle.getSpeed() d = paddle.getDirection() newy = paddle.getCenter()[1] + speed * d[1] # predicted position of paddle center after one tick # Paddle moving UP if d[1] < 0 : newy -= paddle_length/2 boundary = top_wall.getCenter()[1] + paddle_width/2 if newy <= boundary: paddle.setPosition( (paddle.getCenter()[0], boundary + paddle_length/2) ) else: paddle.move() # Paddle moving DOWN elif d[1] > 0 : newy += paddle_length/2 boundary = bottom_wall.getCenter()[1] - paddle_width/2 if newy >= boundary: paddle.setPosition( (paddle.getCenter()[0], boundary - paddle_length/2) ) else: paddle.move() # Player1 movement def player1_move(paddle): if keyboard['s']: paddle.setVelocity(Vector(DOWN).scalarMul(player1_speed).getCoordinates()) paddle_validMove(paddle) elif keyboard['w']: paddle.setVelocity(Vector(UP).scalarMul(player1_speed).getCoordinates()) paddle_validMove(paddle) # Player2 Movement def player2_move(paddle): if player2_auto: # Computer controlled second paddle target = pong_ball.getCenter() current = paddle.getCenter() # move computer paddle only after player has struck the ball and pong_ball.getCenter()[0] > room_width/2 if pong_ball.getDirection()[0] > 0: if target[1] < current[1]: paddle.setVelocity(Vector(UP).scalarMul(player2_speed).getCoordinates()) if target[1] > current[1]: paddle.setVelocity(Vector(DOWN).scalarMul(player2_speed).getCoordinates()) paddle_validMove(paddle) else: # User controlled second paddle if keyboard['down']: paddle.setVelocity(Vector(DOWN).scalarMul(player2_speed).getCoordinates()) paddle_validMove(paddle) elif keyboard['up']: paddle.setVelocity(Vector(UP).scalarMul(player2_speed).getCoordinates()) paddle_validMove(paddle) """ Game Animations """ # Movement of all objects in game def game_movement(): """ Movement and collision detection for all game objects """ # Paddles should move regardless of what the ball is doing player1_move(player1_paddle) player2_move(player2_paddle) # Ball: Check for boundary violations/collisions if not ball_outOfBounds(pong_ball): ball_collisions() pong_ball.move() elif ball_outOfBounds(pong_ball) and not reset_timer.is_running(): p1Score = player1_score p2Score = player2_score calculateScores() ball_reset() # start reset timer if player1_score > p1Score: pong_ball.setVelocity(genRandomVelocity(-1)) elif player2_score > p2Score: pong_ball.setVelocity(genRandomVelocity(1)) """ Game Draw functions """ def draw_scores(canvas): canvas.draw_text(str(player1_score), (room_width/4, (top_margin - paddle_width)/3), 24, "White") canvas.draw_text(str(player2_score), (room_width/4 + room_width/2, (top_margin - paddle_width)/3), 24, "White") def game_over(canvas): def gameover_msg_coordinates(message, text_size): message_width = pong_frame.get_canvas_textwidth(message, text_size) x = (room_center[0] - message_width)/2 y = room_center[1] return (x, y) if player1_score > player2_score: message = "P1 WIN!" coord = gameover_msg_coordinates(message, 24) canvas.draw_text(message, coord, 24, "Red") else: message = "P2 WIN!" coord = gameover_msg_coordinates(message, 24) canvas.draw_text(message, coord, 24, "Blue") def draw_game_static_env(canvas): """ Draw all the static objects in the game environment """ # centerline canvas.draw_line( (room_width/2, top_wall.getCenter()[1]), (room_width/2, bottom_wall.getCenter()[1]), 2, "White") canvas.draw_line([paddle_width, top_wall.getCenter()[1]],[paddle_width, bottom_wall.getCenter()[1]], 1, "White") canvas.draw_line([room_width - paddle_width - 1, top_wall.getCenter()[1]],[room_width - paddle_width, bottom_wall.getCenter()[1]], 1, "White") # walls top_wall.draw(canvas) bottom_wall.draw(canvas) # scores draw_scores(canvas) def draw_ball_paddles(canvas): # Draw Ball pong_ball.draw(canvas) # Draw Paddles player1_paddle.draw(canvas) player2_paddle.draw(canvas) # Main game callback (auto-loop) def draw(canvas): global start_game # draw static elements draw_game_static_env(canvas) # Check if current game is over if not isGameOver(): draw_ball_paddles(canvas) # Check that the game is not blocked, # or new game has not yet been started if start_game: game_movement() else: """ Game has ended, print game results, and freeze canvas until user creates a new game """ draw_ball_paddles(canvas) start_game = False game_over(canvas) """ Event Handlers and Game Loop """ # New Game Button Handler # Mouseclick- Game start def mouseclick_startGame(position): global start_game if not start_game: start_game = True # Enable single player mode, player2 paddle will be controlled by computer def singlePlayer(): global player2_auto game_init() player2_auto = True # 2Player Button Handler def twoPlayer(): global player2_auto game_init() player2_auto = False # Key is pushed and held down def keydown(key): if key == simplegui.KEY_MAP['s']: keyboard['s'] = True elif key == simplegui.KEY_MAP['w']: keyboard['w'] = True if key == simplegui.KEY_MAP['down']: keyboard['down'] = True elif key == simplegui.KEY_MAP['up']: keyboard['up'] = True # Key is released def keyup(key): if key == simplegui.KEY_MAP['s']: keyboard['s'] = False elif key == simplegui.KEY_MAP['w']: keyboard['w'] = False if key == simplegui.KEY_MAP['down']: keyboard['down'] = False elif key == simplegui.KEY_MAP['up']: keyboard['up'] = False """ Game Window Settings """ def gameWindow_init(): global pong_frame, reset_timer # Create Game Window pong_frame = simplegui.create_frame("Pong", roomRect.getWidth(), roomRect.getHeight(), 300) game_instructions = [] game_instructions.append(pong_frame.add_label("Welcome to Pong!")) game_instructions.append(pong_frame.add_label("")) game_instructions.append(pong_frame.add_label("Up and Down arrow keys control the Blue paddle")) game_instructions.append(pong_frame.add_label("")) game_instructions.append(pong_frame.add_label("W and S keys control the Red paddle")) game_instructions.append(pong_frame.add_label("")) game_instructions.append(pong_frame.add_label("Click anywhere in the game window to start")) game_instructions.append(pong_frame.add_label("")) game_instructions.append(pong_frame.add_label("May the force be with you.")) game_instructions.append(pong_frame.add_label("")) # Game Window Buttons and Controls # resetButton = pong_frame.add_button("New Game", new_game, 150) pong_frame.add_button("Single Player (vs Computer)", singlePlayer, 250) pong_frame.add_button("2 Player (Restart)", twoPlayer, 250) # Timers reset_timer = simplegui.create_timer(1000, reset_pause) # Register event handlers pong_frame.set_keydown_handler(keydown) pong_frame.set_keyup_handler(keyup) pong_frame.set_draw_handler(draw) pong_frame.set_mouseclick_handler(mouseclick_startGame) # Start the frame animation gameWindow_init() pong_frame.start()