diff --git a/.gitignore b/.gitignore index cbc2b23..add7f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ local_settings.py instance/ .webassets-cache +# PyCharm +.idea/ + # Scrapy stuff: .scrapy diff --git a/IceCreamGame.py b/IceCreamGame.py new file mode 100644 index 0000000..5e12b36 --- /dev/null +++ b/IceCreamGame.py @@ -0,0 +1,277 @@ +""" + Cloudy with a Chance of Ice Cream is a game in which the player tries to catch falling + ice cream scoops in a cone while avoiding falling obstacles. The player gradually + progresses through the atmosphere and into space. If they make it all the way to Mars + without running into an obstacle, they win. +""" + +import time +from graphics.ice_cream_cone import * +from graphics.background import Background +import random +from graphics.scoop import Scoop +from graphics.obstacles.asteroid import Asteroid +from graphics.obstacles.balloon import Balloon +from graphics.obstacles.bee import Bee +from graphics.obstacles.drone import Drone +from graphics.obstacles.leaf import Leaf + + +class IceCreamGame: + + # ----- Constants ----- + + # Colors + WHITE = (255, 255, 255) + BLACK = (0, 0, 0) + RED = (255, 0, 0) + BLUE = (0, 0, 255) + + OBSTACLE_TYPES = [Leaf, Bee, Balloon, Drone, Asteroid] + + CONE_ACCELERATION = 100 # pixels/second^2 + CONE_ACCELERATION_PLUS = 150 # pixels/second^2 + CONE_DRAG_ACCELERATION = 40 # pixels/second^2 + MAX_CONE_SPEED = 500 # pixels/second + + WINDOW_HEIGHT = 600 # size of screen (px) + WINDOW_WIDTH = 700 # size of screen (px) + WINDOW_TITLE = 'The Best Ice Cream Game Known to Man' + + FPS = 20 # refresh rate (frames per second) + + BG_CHANGE_SPEED_SCALAR = FPS / 10 + BG_CHANGE_SPEED_EXP = 1.5 + + def __init__(self): + pygame.init() # initialize all imported pygame modules + + self.FONT = pygame.font.SysFont(None, 50) # font for messages + + # Initialize drawing canvas and background + self.screen = pygame.display.set_mode((self.WINDOW_WIDTH, self.WINDOW_HEIGHT)) + self.background = Background(self.WINDOW_HEIGHT, self.BG_CHANGE_SPEED_SCALAR, self.BG_CHANGE_SPEED_EXP) + + # Set window title + pygame.display.set_caption(self.WINDOW_TITLE) + + # Set key repeat delay and interval + pygame.key.set_repeat(30, 20) + + # Initialize clock for timing refreshes + self.clock = pygame.time.Clock() + + # Initialize counters to keep track of time (s) till next scoop and obstacle should be released + # (could be done with separate timers, but don't need to be that precise) + self.time_till_next_obstacle_release = 5 + self.time_till_next_scoop_release = 0.5 + + self.cone = IceCreamCone(400, self.WINDOW_HEIGHT - 230) + self.all_obstacles = list() + self.falling_scoops = list() + + # Initialize player's score + self.score = 0 + + + def message_to_screen(self, msg, c, x, y): + """ + Making a message to user function + + :param msg: string you want to send as a message + :param c: color of words for message + :param x: x-coordinate you want to place message + :param y: y-coordinate you want to place message + """ + screen_text = self.FONT.render(msg, True, c) + self.screen.blit(screen_text, [x, y]) + + def check_for_scoop_collision(self): + + # Get the object on the top of the cone stack (normally a scoop, + # unless there are no scoops, in which case it's the cone) + top_of_cone_stack = self.cone.get_top_scoop() + if top_of_cone_stack is None: + top_of_cone_stack = self.cone.cone + + # Check all falling scoops for collision with top item + for scoop in self.falling_scoops: + if pygame.sprite.collide_rect(scoop, top_of_cone_stack): # Caught falling scoop + self.score += 1 + self.falling_scoops.remove(scoop) + self.cone.add_scoop(scoop) + + def launch_game(self): + """ Runs the game loop until the game ends + + :return 0 if the window should stay open until the user closes it, or -1 if the window should be closed immediately + """ + + # Time change from frame to frame + dt = 1 / self.FPS + + while True: + + """ ---- Acceleration of the Cone and Stacked Scoops ---- """ + # Keep track of whether or not the user accelerated the cone with the keyboard + cone_did_accelerate = False + + # First, respond to any user input + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return -1 # Exit immediately + if event.type == pygame.KEYDOWN: + # Pressing shift enables a speed boost + key_mods = pygame.key.get_mods() + accel = self.CONE_ACCELERATION_PLUS \ + if key_mods & pygame.KMOD_LSHIFT or key_mods & pygame.KMOD_RSHIFT \ + else self.CONE_ACCELERATION + + if event.key == pygame.K_LEFT: + self.cone.accelerate(-accel, 0, dt, self.MAX_CONE_SPEED) + cone_did_accelerate = True + elif event.key == pygame.K_RIGHT: + self.cone.accelerate(accel, 0, dt, self.MAX_CONE_SPEED) + cone_did_accelerate = True + elif event.key == pygame.K_q: + return -1 # Exit immediately + + + # If user didn't accelerate cone, simulate drag slowing it down + if not cone_did_accelerate and self.cone.x_velocity != 0: + current_vel_sign = self.cone.x_velocity / math.fabs(self.cone.x_velocity) + self.cone.accelerate(-current_vel_sign*self.CONE_DRAG_ACCELERATION, 0, dt, self.MAX_CONE_SPEED, True) + + """ ---- Update positions of everything ---- """ + self.cone.update_state(dt) + + for i, scoop in enumerate(self.falling_scoops): + scoop.move(0, 1) # TODO Remove hardcoding + # Remove off-screen scoops + if not scoop.is_on_screen(self.WINDOW_WIDTH, self.WINDOW_HEIGHT): + self.falling_scoops.remove(scoop) + for obstacle in self.all_obstacles: + obstacle.update_state(dt) + # Remove off-screen obstacles + if not obstacle.is_on_screen(self.WINDOW_WIDTH, self.WINDOW_HEIGHT): + self.all_obstacles.remove(obstacle) + + self.background.update_state(pygame.time.get_ticks() / 1000) + if self.background.did_reach_end(): + self.message_to_screen("You Win!", self.WHITE, self.WINDOW_WIDTH / 3, self.WINDOW_HEIGHT / 3) + pygame.display.update() + return 0 # Wait for user to exit + + """ ---- Collision Detection ---- """ + # Scoops onto cone (or top scoop on stack) + target_rect = self.cone.get_cone_top_rect() + for scoop in self.falling_scoops: + falling_rect = scoop.get_bottom_rect() + if target_rect.colliderect(falling_rect): + # Scoop collided with top of ice cream cone stack, so add it to cone stack + self.cone.add_scoop(scoop) + self.falling_scoops.remove(scoop) + self.score += 1 + # Obstacles with cone or scoop stack + cone_rect = self.cone.get_rect() + for obstacle in self.all_obstacles: + if cone_rect.colliderect(obstacle.rect): + # Collision! Display message and exit loop + self.message_to_screen("You Lose!", self.RED, self.WINDOW_WIDTH / 3, self.WINDOW_HEIGHT / 3) + pygame.display.update() + return 0 # Wait for user to exit + + elapsed_time = pygame.time.get_ticks() / 1000 # The number of seconds elapsed since the start of the game + + + """ ---- Falling Object Generation ---- """ + # -- Obstacle Generation -- # + # Decrement obstacle release timer + if self.time_till_next_obstacle_release > 0: + self.time_till_next_obstacle_release -= 1 / self.FPS + # Check if timer has reached zero + if self.time_till_next_obstacle_release < 0: + self.release_obstacle(elapsed_time) + self.time_till_next_obstacle_release = random.uniform(3, 10) # TODO Change bounds with time + + # -- Scoop Generation -- # + # Decrement scoop release timer + if self.time_till_next_scoop_release > 0: + self.time_till_next_scoop_release -= 1 / self.FPS + # Check if timer has reached zero + if self.time_till_next_scoop_release < 0: + self.release_scoop() + self.time_till_next_scoop_release = random.uniform(1, 6) # TODO Change bounds with time + + """ ---- Draw everything ---- """ + pygame.display.update() # updates the screen (for every run through the loop) + self.background.draw(self.screen) + for scoop in self.falling_scoops: + scoop.draw(self.screen) + for obstacle in self.all_obstacles: + obstacle.draw(self.screen) + self.cone.draw(self.screen) + + # Display score + self.message_to_screen('Score: %i' % self.score, self.BLUE, 10, 10) + + self.clock.tick(self.FPS) # Pause to maintain the given frame rate + + def release_obstacle(self, time_elapsed): + """ Release an obstacle appropriate for the given time into the game + + :param time_elapsed: number of seconds elapsed during the game + """ + # Calculate background position + y = self.BG_CHANGE_SPEED_SCALAR * math.pow(time_elapsed, self.BG_CHANGE_SPEED_EXP) + thresholds = [400*x for x in range(len(self.OBSTACLE_TYPES))] + + obs_type = self.OBSTACLE_TYPES[0] + for i in range(len(thresholds)-1, 0, -1): + if y > thresholds[i]: + obs_type = self.OBSTACLE_TYPES[i] + break + + rand_x_loc = random.randint(0, self.WINDOW_WIDTH) + print('Starting at {}'.format(rand_x_loc)) + if obs_type == Leaf: + obs = Leaf(rand_x_loc, 0) + elif obs_type == Bee: + obs = Bee(rand_x_loc, 0) + elif obs_type == Drone: + obs = Drone(rand_x_loc, 0) + elif obs_type == Balloon: + obs = Balloon(rand_x_loc, 0) + elif obs_type == Asteroid: + obs = Asteroid(rand_x_loc, 0) + else: + raise Exception('Invalid obstacle type: %s' % obs_type) + + obs.rect.bottom = 1 # Place just at top of screen + self.all_obstacles.append(obs) + + def release_scoop(self): + """ Release a falling ice cream scoop """ + rand_x = random.randint(0, self.WINDOW_WIDTH) + scoop = Scoop(rand_x, 0) + scoop.rect.bottom = 1 # Place just at top of screen + self.falling_scoops.append(scoop) + + + @staticmethod + def wait_for_close(): + """ Pause execution until the user clicks Close """ + time.sleep(2) + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + + +if __name__ == '__main__': + game = IceCreamGame() + exit_code = game.launch_game() + if exit_code == 0: # Player won or lost, wait to exit + game.wait_for_close() + pygame.quit() # uninitializes pygame + quit() # must have a quit diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 61ec120..45b46ee --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# InteractiveProgramming -This is the base repo for the interactive programming project for Software Design, Spring 2016 at Olin College. +## Cloudy with a Chance of Ice Cream + +In Cloudy with a Chance of Ice Cream, you catch falling ice cream scoops while avoiding various atmospheric obstacles. Colliding with an obstacle results in a loss of health. As you continue to ascend, the environment changes and new obstacles emerge. + +This game was created for [Mini Project 4 - Interactive Programming](https://sd17spring.github.io//assignments/mini-project-4-interactive-visualization/) of Software Design at [Olin College of Engineering](http://olin.edu/). + +#### To Play +Run **python3 IceCreamGame.py**. Note: the pygame library must be installed. + +#### Reflection + +Our reflection and summary can be found [here](ReflectionSummary.pdf). + + + +## Initial Project Proposal + +#### Minimum Viable Product + +Our MVP is a game in which the player moves an ice cream cone back and forth along the x-axis using the arrow keys in an attempt to catch ice cream scoops falling vertically. Obstacles, such as asteroids, will also fall vertically, and the player will need to avoid catching the obstacles lest they sustain damage. It will have one environment, outer space, which will be represented by an appropriately themed background image. + +#### Stretch Goals +We foresee our MVP being improved by implementing any of the following additions: + + * More environments ("levels"): going higher (or starting lower) in altitude would provide different background and obstacles + * Arrow keys accelerate cone (rather than move at constant velocity) + * Wobbling stack of scoops when cone accelerates. Player needs to be careful not to move too quickly. + * If high level entailed flying by planets, incorporate gravity from surrounding planets and give scoops an acceleration in x-direction + * Dynamic and adaptable difficulty thresholding + +#### Individual Learning Goals + +###### Allison + + * Gain more experience with the pygame library + * Become more familiar with serious user input interactions + +###### Kyle + + * Gain experience with the pygame library + * Learn how to implement graceful environment transitions + * Learn how to implement realistic physics simulations (gravity, ice cream scoops sensitive to toppling) in a game + +## Libraries +Our game will make heavy use of the [pygame](http://pygame.org/) library. + +## Timeline +By our mid-project check-in, we plan to have the following done: +* All classes defined +* Good understanding of pygame options +* Identified basic physics implementations and have idea of how to implement +* Basic user interface +* Idea of how to change the background scene +* Detailed breakdown of all the project tasks + +## Predicted Roadblocks +We feel we might run into trouble with the following: + * Implementing stretch goals and physics + * Implementing good graphics diff --git a/ReflectionSummary.pdf b/ReflectionSummary.pdf new file mode 100644 index 0000000..2c6ca0d Binary files /dev/null and b/ReflectionSummary.pdf differ diff --git a/assets/img/background.jpg b/assets/img/background.jpg new file mode 100755 index 0000000..bf63190 Binary files /dev/null and b/assets/img/background.jpg differ diff --git a/assets/img/cone.png b/assets/img/cone.png new file mode 100755 index 0000000..978d619 Binary files /dev/null and b/assets/img/cone.png differ diff --git a/assets/img/ice-cream-scoops-ctr.jpg b/assets/img/ice-cream-scoops-ctr.jpg new file mode 100644 index 0000000..bb89591 Binary files /dev/null and b/assets/img/ice-cream-scoops-ctr.jpg differ diff --git a/assets/img/obstacles/asteroid.png b/assets/img/obstacles/asteroid.png new file mode 100644 index 0000000..c53fe3a Binary files /dev/null and b/assets/img/obstacles/asteroid.png differ diff --git a/assets/img/obstacles/balloon.png b/assets/img/obstacles/balloon.png new file mode 100644 index 0000000..4ebc26b Binary files /dev/null and b/assets/img/obstacles/balloon.png differ diff --git a/assets/img/obstacles/bee.png b/assets/img/obstacles/bee.png new file mode 100644 index 0000000..1a5a9f6 Binary files /dev/null and b/assets/img/obstacles/bee.png differ diff --git a/assets/img/obstacles/drone.png b/assets/img/obstacles/drone.png new file mode 100644 index 0000000..5aaa861 Binary files /dev/null and b/assets/img/obstacles/drone.png differ diff --git a/assets/img/obstacles/leaf.png b/assets/img/obstacles/leaf.png new file mode 100644 index 0000000..3c4d9d1 Binary files /dev/null and b/assets/img/obstacles/leaf.png differ diff --git a/assets/img/scoops/choc.png b/assets/img/scoops/choc.png new file mode 100644 index 0000000..c971251 Binary files /dev/null and b/assets/img/scoops/choc.png differ diff --git a/assets/img/scoops/mint.png b/assets/img/scoops/mint.png new file mode 100644 index 0000000..26aa7ab Binary files /dev/null and b/assets/img/scoops/mint.png differ diff --git a/assets/img/scoops/pink.png b/assets/img/scoops/pink.png new file mode 100644 index 0000000..a72ece2 Binary files /dev/null and b/assets/img/scoops/pink.png differ diff --git a/assets/img/scoops/scoop-white.png b/assets/img/scoops/scoop-white.png new file mode 100755 index 0000000..1d99b01 Binary files /dev/null and b/assets/img/scoops/scoop-white.png differ diff --git a/assets/img/scoops/strawberry.png b/assets/img/scoops/strawberry.png new file mode 100644 index 0000000..c41e460 Binary files /dev/null and b/assets/img/scoops/strawberry.png differ diff --git a/assets/img/sky.jpg b/assets/img/sky.jpg new file mode 100644 index 0000000..419bc46 Binary files /dev/null and b/assets/img/sky.jpg differ diff --git a/assets/img/space_bg.png b/assets/img/space_bg.png new file mode 100644 index 0000000..469c7fe Binary files /dev/null and b/assets/img/space_bg.png differ diff --git a/graphics/__init__.py b/graphics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphics/background.py b/graphics/background.py new file mode 100644 index 0000000..39a3918 --- /dev/null +++ b/graphics/background.py @@ -0,0 +1,58 @@ +""" The visual background for the game """ +from helpers import * +import math + +class Background(pygame.sprite.Sprite): + + def __init__(self, window_height, move_speed_scalar, move_speed_exponent): + """ Initializes a Background object + + :param window_height: the height of the Surface the image is covering + """ + pygame.sprite.Sprite.__init__(self) + + # Load the single giant background image + self.image, self.rect = load_image(os.path.join('assets', 'img', 'background.jpg')) + + # Position the bottom of the image at the bottom of the screen + self.rect.bottom = window_height + self.rect.left = 0 + + self.window_height = window_height + self.move_speed_scalar = move_speed_scalar + self.move_speed_exponent = move_speed_exponent + + def shift_down(self, dy): + """ Shifts the background image down + + :param dy: the number of pixels to shift the image down by + """ + self.rect.bottom += dy + + def set_y(self, y_pos): + """ Sets the y-position of the image relative to its starting position + + :param y_pos: the number of pixels the image should be shifted down from its starting location + """ + self.rect.bottom = self.window_height + y_pos + + def update_state(self, elapsed_time): + """ Updates the state (position, etc.) of the background + + :param elapsed_time: the amount of time (s) elapsed since the start of the game + """ + self.rect.bottom = self.window_height + math.floor(self.move_speed_scalar * math.pow(elapsed_time, self.move_speed_exponent)) + + def did_reach_end(self): + """ Checks if the image has been scrolled all the way to the end + + :return True if image has been scrolled all the way, False otherwise + """ + return self.rect.top >= 0 + + def draw(self, screen): + """ Draws the image on the screen + + :param screen: the Surface to draw on + """ + screen.blit(self.image, self.rect) diff --git a/graphics/graphic.py b/graphics/graphic.py new file mode 100644 index 0000000..1ab315c --- /dev/null +++ b/graphics/graphic.py @@ -0,0 +1,47 @@ +""" Abstract base class for graphical objects + + Note: Since it is an abstract base class (ABC), one cannot create objects + of Graphic directly. Rather, one must create a new class that extends + Graphic and implement all of the required methods. +""" +from abc import ABCMeta +import pygame + + +class Graphic(pygame.sprite.Sprite): + __metaclass__ = ABCMeta + + x_pos = None # int - the x-position of the graphic + y_pos = None # int - the y-position of the graphic + scale = None # int - a scalar used to adjust the size of the graphic + velocity = None # (int,int) - the velocity of the object in m/s + + def __init__(self): + pygame.sprite.Sprite.__init__(self) + + def move(self, delta_x, delta_y): + """ Shifts the graphic + + delta_x: int - number of pixels to shift in the x-direction + delta_y: int - number of pixels to shift in the y-direction + """ + if hasattr(self, 'rect'): + self.rect.move_ip(delta_x, delta_y) + else: + raise Exception("Attribute 'rect' not defined for %s" % self.__class__) + + def is_on_screen(self, screen_width, screen_height, margin=0): + """ Checks to see if the item is on or off the screen + + :param screen_width: the width of the Surface the object being is drawn on + :param screen_height: the height of the Surface the object being is drawn on + :param margin: an optional shift of the screen edges. Positive shifts the screen edges in, negative shifts out. + :returns True if the object is at least partially within the bounds of the screen (w/margin), False if it is entirely outside. + """ + if hasattr(self, 'rect'): + return not (self.rect.right < margin + or self.rect.left > screen_width - margin + or self.rect.bottom < margin + or self.rect.top > screen_height - margin) + else: + raise Exception("Attribute 'rect' not defined for %s" % self.__class__) diff --git a/graphics/ice_cream_cone.py b/graphics/ice_cream_cone.py new file mode 100644 index 0000000..65c41db --- /dev/null +++ b/graphics/ice_cream_cone.py @@ -0,0 +1,142 @@ +from graphics.graphic import Graphic +from helpers import * +import math + + +class IceCreamCone(Graphic): + + SCOOP_CONE_OFFSET = 50 # draw the first scoop this many pixels below the top of the cone + SCOOP_SCOOP_OFFSET = 50 # draw scoops this many pixels below the top of the previous scoop + + SCOOP_LANDING_ZONE = 20 # consider a scoop to have landed on top of the cone or another scoop if it is within + # this many pixels of the top of the cone or top scoop (if any) + + def __init__(self, x_pos, y_pos): + """ Initializes an IceCreamCone object + + scale: an int used to scale the width and height to control the size + x_pos: the starting x-position of the cone + y_pos: the starting y-position of the cone + """ + Graphic.__init__(self) + # Initialize our velocities to zero + self.x_velocity = 0 + self.y_velocity = 0 + # Create a new sprite group to handle drawing all our sprites + self.sprite_group = pygame.sprite.OrderedUpdates() + # Create a new empty cone and add it to our sprite group + self.cone = EmptyCone(x_pos, y_pos) + self.sprite_group.add(self.cone) + # Create a new list to keep track of the scoops on the cone + self.scoops = list() + + def accelerate(self, ax, ay, dt, max_speed, due_to_drag=False): + """ Accelerate the cone and its contents in the corresponding direction + + :param ax: the acceleration component in the x-direction + :param ay: the acceleration component in the y-direction + :param dt: the amount of time for which to accelerate the cone + :param max_speed: the maximum speed at which the cone can travel + :param due_to_drag: whether or not this acceleration is due to drag, in + which case the velocity should not switch signs, but rather go to zero + """ + dv = ax * dt + # Check if we're decelerating due to drag and reaching 0 + if due_to_drag and math.fabs(self.x_velocity) < math.fabs(dv): + self.x_velocity = 0 + else: + self.x_velocity += dv + # Check if we're now going faster than our max allowed speed + if math.fabs(self.x_velocity) > max_speed: + self.x_velocity = max_speed * (self.x_velocity / math.fabs(self.x_velocity)) + locations = [(s.rect.x, s.rect.y) for s in self.sprite_group.sprites()] + return locations[-1] # returns a tuple of the x and y location of the top left cornor of the sprite group + + def update_state(self, dt): + """ Update the state (position, etc.) of the cone given a period of elapsed time + + :param dt: the amount of time elapsed since the last update_state call + """ + dx = math.ceil(self.x_velocity*dt) + for sprite in self.sprite_group.sprites(): # Move each sprite in our group + sprite.rect.move_ip(dx, 0) + + + def add_scoop(self, scoop): + """ + Adds a scoop to the top of the stack on the cone. + + Once a scoop has been handed off to the cone, the cone will handle + redrawing it in the proper location. All other code should dispose + of any references to the object to avoid accidental manipulations. + + scoop: scoop sprite to be placed on top + """ + # ----- Position the scoop correctly ----- + # Center the scoop over the cone + scoop.rect.centerx = self.cone.rect.centerx + # Determine where the scoop should be placed such that it sits atop the stack + effective_scoop_height = scoop.rect.height - self.SCOOP_SCOOP_OFFSET + scoop_count = len(self.scoops) + if scoop_count > 0: + current_top = self.scoops[-1].rect.top + offset_from_cone_bottom = self.cone.rect.bottom - current_top - self.SCOOP_SCOOP_OFFSET + else: + current_top = self.cone.rect.top + offset_from_cone_bottom = self.cone.rect.bottom - current_top - self.SCOOP_CONE_OFFSET + + if len(self.scoops) > 2: + # Shift all the scoops and cone down + for item in self.sprite_group.sprites(): + item.rect.move_ip(0, effective_scoop_height) + # Put the scoop on top + scoop.rect.bottom = self.cone.rect.bottom - offset_from_cone_bottom + + #adds to sprite group and appends to scoops object + self.scoops.append(scoop) + self.sprite_group.add(scoop) + + def get_top_scoop(self): + """" Returns the scoop at the top of the stack, None if empty """ + return self.scoops[-1] if len(self.scoops) > 0 else None + + def get_rect(self): + """" Returns the rect enclosing the entire cone and scoop stack """ + r = self.cone.rect.copy() + for scoop in self.scoops: + r.union_ip(scoop.rect) + return r + + def get_cone_top_rect(self): + """" Returns a rect which represents the top of the cone (to use + when determining if a scoop has landed on the top of the cone + or the top scoop). + """ + # Get the rect of whatever object is at the top of our stack (top scoop if any, cone otherwise) + r_model = self.scoops[-1].rect if len(self.scoops) > 0 else self.cone.rect + # Return a new rect of the proper height and aligned with the top of the old rect + return pygame.Rect(r_model.left, r_model.top, r_model.width, self.SCOOP_LANDING_ZONE) + + def draw(self, screen): + """ + Draws the ice cream cone and stacked scoops on the screen + + screen: screen object to be drawn on + """ + self.sprite_group.draw(screen) + + + + +class EmptyCone(Graphic): + + def __init__(self, x_pos, y_pos): + """ Initializes an EmptyCone object + + x_pos: the starting x-position of the cone + y_pos: the starting y-position of the cone + """ + Graphic.__init__(self) + self.image, self.rect = load_image(os.path.join('assets', 'img', 'cone.png'), -1) + self.rect.x = x_pos + self.rect.y = y_pos diff --git a/graphics/obstacles/__init__.py b/graphics/obstacles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphics/obstacles/asteroid.py b/graphics/obstacles/asteroid.py new file mode 100644 index 0000000..a683772 --- /dev/null +++ b/graphics/obstacles/asteroid.py @@ -0,0 +1,41 @@ +from graphics.obstacles.obstacle import Obstacle +from helpers import * +import random +import math + + +class Asteroid(Obstacle): + + MAX_HORIZONTAL_SPEED = 30 # pixels/second + MIN_HORIZONTAL_SPEED = 12 # pixels/second + FALLING_SPEED = 30 # pixels/second + + + def __init__(self, x_pos, y_pos): + """ Initializes a new Scoop object at the given position and scale. + + x_pos: int - the x-coordinate of the top-left corner + y_pos: int - the y-coordinate of the top-left corner + scale: int - scales the size of the scoop + """ + Obstacle.__init__(self, self.FALLING_SPEED, 0) + self.image, self.rect = load_image(os.path.join('assets', 'img', 'obstacles', 'asteroid.png'), -1) + + self.rect.x = x_pos + self.rect.y = y_pos + # Pick a random direction (left or right) and speed within the specified range + direction = 1 if random.randint(0, 1) == 1 else -1 + self.x_velocity = random.randint(self.MIN_HORIZONTAL_SPEED, self.MAX_HORIZONTAL_SPEED) * direction + + + def update_state(self, dt): + """ Update the object position and any other state attributes + + :param dt: (int) the amount of time that has passed since the last call + """ + dx = math.ceil(self.x_velocity * dt) + # Also descend + dy = math.ceil(self.y_velocity * dt) + self.rect.move_ip(dx, dy) + + diff --git a/graphics/obstacles/balloon.py b/graphics/obstacles/balloon.py new file mode 100644 index 0000000..bc403fe --- /dev/null +++ b/graphics/obstacles/balloon.py @@ -0,0 +1,23 @@ +from graphics.obstacles.obstacle import Obstacle +from helpers import * +import random + + +class Balloon(Obstacle): + + STARTING_MAX_HORIZONTAL_SPEED = 30 # pixels/second + MAX_WIND_SPEED = 40 # pixels/second + FALLING_SPEED = 10 # pixels/second + + def __init__(self, x_pos, y_pos): + """ Initializes a new Scoop object at the given position and scale. + + :param x_pos: int - the x-coordinate of the top-left corner + :param y_pos: int - the y-coordinate of the top-left corner + """ + Obstacle.__init__(self, self.FALLING_SPEED, self.MAX_WIND_SPEED) + self.image, self.rect = load_image(os.path.join('assets', 'img', 'obstacles', 'balloon.png'), -1) + self.rect.x = x_pos + self.rect.y = y_pos + self.x_velocity = random.uniform(-1, 1) + self.y_velocity = self.FALLING_SPEED diff --git a/graphics/obstacles/bee.py b/graphics/obstacles/bee.py new file mode 100644 index 0000000..e3565d8 --- /dev/null +++ b/graphics/obstacles/bee.py @@ -0,0 +1,23 @@ +from graphics.obstacles.obstacle import Obstacle +from helpers import * + + +class Bee(Obstacle): + + STARTING_MAX_HORIZONTAL_SPEED = 40 # pixels/second + MAX_WIND_SPEED = 20 # pixels/second + FALLING_SPEED = 15 # pixels/second + + def __init__(self, x_pos, y_pos): + """ Initializes a new Scoop object at the given position and scale. + + x_pos: int - the x-coordinate of the top-left corner + y_pos: int - the y-coordinate of the top-left corner + scale: int - scales the size of the scoop + """ + Obstacle.__init__(self, self.FALLING_SPEED, self.MAX_WIND_SPEED) + self.image, self.rect = load_image(os.path.join('assets', 'img', 'obstacles', 'bee.png'), -1) + + self.rect.x = x_pos + self.rect.y = y_pos + self.y_velocity = self.FALLING_SPEED diff --git a/graphics/obstacles/drone.py b/graphics/obstacles/drone.py new file mode 100644 index 0000000..b036984 --- /dev/null +++ b/graphics/obstacles/drone.py @@ -0,0 +1,22 @@ +from graphics.obstacles.obstacle import Obstacle +from helpers import * + + +class Drone(Obstacle): + + STARTING_MAX_HORIZONTAL_SPEED = 30 # pixels/second + MAX_WIND_SPEED = 10 # pixels/second + FALLING_SPEED = 10 # pixels/second + + def __init__(self, x_pos, y_pos): + """ Initializes a new Scoop object at the given position and scale. + + x_pos: int - the x-coordinate of the top-left corner + y_pos: int - the y-coordinate of the top-left corner + scale: int - scales the size of the scoop + """ + Obstacle.__init__(self, self.FALLING_SPEED, self.MAX_WIND_SPEED) + self.image, self.rect = load_image(os.path.join('assets', 'img', 'obstacles', 'drone.png'), -1) + + self.rect.x = x_pos + self.rect.y = y_pos diff --git a/graphics/obstacles/leaf.py b/graphics/obstacles/leaf.py new file mode 100644 index 0000000..3a9fe90 --- /dev/null +++ b/graphics/obstacles/leaf.py @@ -0,0 +1,22 @@ +from graphics.obstacles.obstacle import Obstacle +from helpers import * + + +class Leaf(Obstacle): + + STARTING_MAX_HORIZONTAL_SPEED = 30 # pixels/second + MAX_WIND_SPEED = 30 # pixels/second + FALLING_SPEED = 10 # pixels/second + + def __init__(self, x_pos, y_pos): + """ Initializes a new Scoop object at the given position and scale. + + x_pos: int - the x-coordinate of the top-left corner + y_pos: int - the y-coordinate of the top-left corner + scale: int - scales the size of the scoop + """ + Obstacle.__init__(self, self.FALLING_SPEED, self.MAX_WIND_SPEED) + self.image, self.rect = load_image(os.path.join('assets', 'img', 'obstacles', 'leaf.png'), -1) + self.rect.x = x_pos + self.rect.y = y_pos + self.y_velocity = self.FALLING_SPEED diff --git a/graphics/obstacles/obstacle.py b/graphics/obstacles/obstacle.py new file mode 100644 index 0000000..fab058b --- /dev/null +++ b/graphics/obstacles/obstacle.py @@ -0,0 +1,39 @@ +""" Abstract base class for obstacle objects + + Note: Since it is an abstract base class (ABC), one cannot create objects + of Obstacle directly. Rather, one must create a new class that extends + Obstacle and implement all of the required methods. +""" +from graphics.graphic import Graphic +import random +import math + + +class Obstacle(Graphic): + + + def __init__(self, y_velocity, max_wind_speed): + Graphic.__init__(self) + self.x_velocity = 0 + self.y_velocity = y_velocity + self.max_wind_speed = max_wind_speed + + def draw(self, screen): + """ Draws the scoop on the screen + + :param screen: a pygame Surface to draw on + """ + screen.blit(self.image, (self.rect.x,self.rect.y)) # The input tuple is the location in x and y respectively + + def update_state(self, dt): + """ Update the object position and any other state attributes + + :param dt: (int) the amount of time that has passed since the last call + """ + # Simulate wind blowing back and forth + wind_effect = random.randrange(-self.max_wind_speed, self.max_wind_speed) + self.x_velocity += wind_effect + dx = math.ceil(self.x_velocity * dt) + # Also descend + dy = math.ceil(self.y_velocity * dt) + self.rect.move_ip(dx, dy) diff --git a/graphics/scoop.py b/graphics/scoop.py new file mode 100644 index 0000000..91df0e9 --- /dev/null +++ b/graphics/scoop.py @@ -0,0 +1,50 @@ +from graphics.graphic import Graphic +from helpers import * +import random + + +class Scoop(Graphic): + + SCOOP_LANDING_ZONE = 20 # The vertical distance from the bottom of the scoop to be used when determining + # if the scoop has landed on top of the cone + + def __init__(self, x_pos, y_pos, kind=None): + """ Initializes a new Scoop object at the given position and scale. + + rect.x: int - the x-coordinate of the top-left corner + rect.y: int - the y-coordinate of the top-left corner + """ + Graphic.__init__(self) + + types_of_cream = ['mint.png', 'scoop-white.png', 'pink.png', 'choc.png'] + + if kind is None: + self.kind = random.choice(types_of_cream) + else: + self.kind = kind + self.image, self.rect = load_image(os.path.join('assets', 'img', 'scoops', self.kind), -1) + + self.rect.x = x_pos + self.rect.y = y_pos + + def draw(self, screen): + """ Draws the scoop on the screen + + :param screen: a pygame Surface to draw on + """ + screen.blit(self.image, (self.rect.x, self.rect.y)) + + def move(self, delta_x, delta_y): + """ Move the scoop by the specified amounts + + :param delta_x: the number of pixels to move in the x-direction (right is positive) + :param delta_y: the number of pixels to move in the y-direction (down in positive) + """ + self.rect.move_ip(delta_x, delta_y) + + def get_bottom_rect(self): + """ Returns a rect which represents the bottom of the scoop (to use + when determining if a scoop has landed on the top of the cone + or the top scoop). + """ + return pygame.Rect(self.rect.left, self.rect.bottom - self.SCOOP_LANDING_ZONE, self.rect.width, self.SCOOP_LANDING_ZONE) diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..336dd38 --- /dev/null +++ b/helpers.py @@ -0,0 +1,20 @@ +#! /usr/bin/env python + +import os +import pygame +from pygame.locals import * + + +def load_image(name, colorkey=None): + fullname = os.path.join(name) + try: + image = pygame.image.load(fullname) + except pygame.error: + print ('Cannot load image:', fullname) + raise SystemExit + image = image.convert() + if colorkey is not None: + if colorkey is -1: + colorkey = image.get_at((0,0)) + image.set_colorkey(colorkey, RLEACCEL) + return image, image.get_rect()