diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..f1d417c --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pygame = "*" + +[dev-packages] + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..93085e1 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,46 @@ +{ + "_meta": { + "hash": { + "sha256": "04d5136a2e3e1a7589c6313b58b439879c213afd077d2d6580213f4255dcef7d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pygame": { + "hashes": [ + "sha256:22d5195a72b9a300cb7e87d7bde1373930a02b1054e95bc431d26a321ae15d07", + "sha256:2a8292d0b5a67e0b96d2cae09bbd8252e99daffe95fd88a00166b92fb6497b40", + "sha256:2ee479b0120cc82f04f7c41a09d75fc85c9992ed1a26c9527fb2b7b0637e6739", + "sha256:4847550da37d3bc702e0201a035de005f184b19e86625a3c70ab1d318317ee91", + "sha256:4d4985d0e0e5470c5ef872d65a863f1722d85a136327eb3bb5dcafc2853f2e55", + "sha256:5cf670378c8aabdf1bdf283eeec690c93f95dfe8de77c66bc837c91274c8ca49", + "sha256:643de45518d11cb7784e3fe77aef43fe3cb7e0d6b495ad4361ade931f16c334b", + "sha256:66746c32d21d8193ab066fdf90da96ed993c4818adb13bf4eaa472f306f7f421", + "sha256:73c86fdbbfc5ae77d0271fb0964f4a3fc639cc2277ffeb60ee4e981fb0d27640", + "sha256:751021819bdc0cbe5cbd51904abb6ff9e9aee5b0e8955af02284d0e77d6c9ec2", + "sha256:792f8d1a455d5d1de9c8f2cc051c6dac2b90705afc4cf9fe4933aeb296b0ae3f", + "sha256:831c906d4a70aa58f9382e79671525805583b58ac8c895b4c4bd066e8858a1a1", + "sha256:834ee27388563f15e6c736aa4850e4cf95609f67056ee73ff646528bb658e79e", + "sha256:925e6be8e8c3f1cec016f5a68f369491ae34600e04633759e0e07780ce7bd083", + "sha256:96a8f61c729d82576ee3d5cfe3b0b91e7b52a4fbf3d618a3b659347eeb4fd937", + "sha256:97615fb075808b4709f6115e3a11a253bfac78614f430bededbc0ac76a5100af", + "sha256:c49ece0e4ba71be90ed4bf6a905c35e5c1a95c4c0b0b42eaff74ab69c5793847", + "sha256:e8626d4d4e7617ebf103d96a493b9cbe53f9b39cd46ecbff232e33ba4a082530", + "sha256:ea7da8dd7fbc12becccf691b4c76fbb381e369e124c30e21f9ed263b6e7e139a" + ], + "index": "pypi", + "version": "==1.9.3" + } + }, + "develop": {} +} diff --git a/src/ball.py b/src/ball.py index 20e2792..df08733 100644 --- a/src/ball.py +++ b/src/ball.py @@ -1,14 +1,19 @@ -import math +# import math from pygame.math import Vector2 from pygame import Rect +from block import BreakableBlock from block import KineticBlock +import sys + + class Ball: """ base class for bouncing objects """ + def __init__(self, bounds, position, velocity, color, radius): self.position = position self.velocity = velocity @@ -18,23 +23,26 @@ def __init__(self, bounds, position, velocity, color, radius): self.collision_rectangle = self.update_rectangle() def update_rectangle(self): - return Rect(self.position.x - self.radius, - self.position.y - self.radius, - self.radius*2, self.radius*2) + return Rect( + self.position.x - self.radius, + self.position.y - self.radius, + self.radius * 2, + self.radius * 2, + ) def update(self, **kwargs): - if self.position.x <= 0 + self.radius: # screen width + if self.position.x <= 0 + self.radius: # screen width self.position.x = self.radius + 1 self.velocity.x *= -1 if self.position.x >= self.bounds[0] - self.radius: self.position.x = self.bounds[0] - self.radius - 1 self.velocity.x *= -1 - if self.position.y <= 0 + self.radius: # screen height - self.position.y = self.radius + 1 - self.velocity.y *= -1 + if self.position.y <= 0 + self.radius: # screen height + print("You win!") + sys.exit() if self.position.y >= self.bounds[1] - self.radius: - self.position.y = self.bounds[1] - self.radius - 1 - self.velocity.y *= -1 + print("You lose!") + sys.exit() self.position += self.velocity self.collision_rectangle = self.update_rectangle() @@ -45,12 +53,19 @@ def check_collision(self): def draw(self, screen, pygame): # cast x and y to int for drawing - pygame.draw.circle(screen, self.color, [int(self.position.x), int(self.position.y)], self.radius) + pygame.draw.circle( + screen, + self.color, + [int(self.position.x), int(self.position.y)], + self.radius, + ) + class GameBall(Ball): """ A ball that collides with blocks """ + def __init__(self, mass, object_list, bounds, position, velocity, color, radius): self.object_list = object_list self.mass = mass @@ -58,7 +73,7 @@ def __init__(self, mass, object_list, bounds, position, velocity, color, radius) def collide_with_ball(self, object, relative_vector): - #TODO: Calculate the correct position and move there directly + # TODO: Calculate the correct position and move there directly while relative_vector.length() <= self.radius + object.radius: self.position += relative_vector.normalize() object.position -= relative_vector.normalize() @@ -78,90 +93,135 @@ def collide_with_ball(self, object, relative_vector): def collide_with_rectangle(self, object): # This function is called after a first-pass test, that is the collision - # rectangles overlap. - + # rectangles overlap. + left, right, top, bottom = False, False, False, False # TODO: This can probably be optimized if ( - object.position.x > self.position.x and - object.position.x - object.rectangle.width/2 <= self.position.x + self.radius and - self.position.y <= object.position.y+object.rectangle.height/2 and - self.position.y >= object.position.y - object.rectangle.height/2 + object.position.x > self.position.x + and object.position.x - object.rectangle.width / 2 + <= self.position.x + self.radius + and self.position.y <= object.position.y + object.rectangle.height / 2 + and self.position.y >= object.position.y - object.rectangle.height / 2 ): left = True if ( - object.position.x < self.position.x and - object.position.x + object.rectangle.width/2 >= self.position.x - self.radius and - self.position.y <= object.position.y+object.rectangle.height/2 and - self.position.y >= object.position.y - object.rectangle.height/2 + object.position.x < self.position.x + and object.position.x + object.rectangle.width / 2 + >= self.position.x - self.radius + and self.position.y <= object.position.y + object.rectangle.height / 2 + and self.position.y >= object.position.y - object.rectangle.height / 2 ): right = True if ( - object.position.y > self.position.y and - object.position.y - object.rectangle.height/2 <= self.position.y + self.radius and - self.position.x <= object.position.x+object.rectangle.width/2 and - self.position.x >= object.position.x - object.rectangle.width/2 + object.position.y > self.position.y + and object.position.y - object.rectangle.height / 2 + <= self.position.y + self.radius + and self.position.x <= object.position.x + object.rectangle.width / 2 + and self.position.x >= object.position.x - object.rectangle.width / 2 ): top = True if ( - object.position.y < self.position.y and - object.position.y + object.rectangle.width/2 >= self.position.y - self.radius and - self.position.x <= object.position.x+object.rectangle.width/2 and - self.position.x >= object.position.x - object.rectangle.width/2 + object.position.y < self.position.y + and object.position.y + object.rectangle.width / 2 + >= self.position.y - self.radius + and self.position.x <= object.position.x + object.rectangle.width / 2 + and self.position.x >= object.position.x - object.rectangle.width / 2 ): bottom = True test = left + right + top + bottom - + if test == 1: object.touched_by_ball = True + if isinstance(object, BreakableBlock): + object.got_hit() + if object.hp == 0: + self.object_list.remove(object) # the ball has collided with an edge # TODO: # fix sticky edges if left or right: self.velocity.x *= -1 if left: - self.position.x = object.position.x - object.rectangle.width/2 - self.radius - 1 + self.position.x = ( + object.position.x - object.rectangle.width / 2 - self.radius - 1 + ) else: - self.position.x = object.position.x + object.rectangle.width/2 + self.radius + 1 + self.position.x = ( + object.position.x + object.rectangle.width / 2 + self.radius + 1 + ) if top or bottom: self.velocity.y *= -1 if top: - self.position.y = object.position.y - object.rectangle.height/2 - self.radius - 1 + self.position.y = ( + object.position.y + - object.rectangle.height / 2 + - self.radius + - 1 + ) else: - self.position.y = object.position.y + object.rectangle.height/2 + self.radius + 1 + self.position.y = ( + object.position.y + + object.rectangle.height / 2 + + self.radius + + 1 + ) elif test == 4: # TODO: Better error handling - print('error: ball inside rectangle') + print("error: ball inside rectangle") elif test == 0: # We are at a corner. Either it narrowly missed, or it hit the corner corners = [ - Vector2(object.position.x - object.rectangle.width/2, object.position.y - object.rectangle.height/2), - Vector2(object.position.x + object.rectangle.width/2, object.position.y - object.rectangle.height/2), - Vector2(object.position.x - object.rectangle.width/2, object.position.y + object.rectangle.height/2), - Vector2(object.position.x + object.rectangle.width/2, object.position.y + object.rectangle.height/2) + Vector2( + object.position.x - object.rectangle.width / 2, + object.position.y - object.rectangle.height / 2, + ), + Vector2( + object.position.x + object.rectangle.width / 2, + object.position.y - object.rectangle.height / 2, + ), + Vector2( + object.position.x - object.rectangle.width / 2, + object.position.y + object.rectangle.height / 2, + ), + Vector2( + object.position.x + object.rectangle.width / 2, + object.position.y + object.rectangle.height / 2, + ), ] for corner in corners: relative_vector = self.position - corner if relative_vector.length() <= self.radius: object.touched_by_ball = True - # Create a dummy object to make use of ball to ball collision, because the math is the same - # Give it a velocity of the same magnitude as the current ball to cause it to reflect at - # the same speed - stand_in = Ball(self.bounds, corner, Vector2(0, self.velocity.length()), [0,0,0], 0) + if isinstance(object, BreakableBlock): + object.got_hit() + if object.hp == 0: + self.object_list.remove(object) + # Create a dummy object to make use of ball to ball collision, + # because the math is the same. Give it a velocity of the + # same magnitude as the current ball to cause it to reflect at the + # same speed + stand_in = Ball( + self.bounds, + corner, + Vector2(0, self.velocity.length()), + [0, 0, 0], + 0, + ) self.collide_with_ball(stand_in, relative_vector) def check_collision(self): # Warning!: This is a primitive method of collision detection # Consider time complexity when adding more of this type index = self.object_list.index(self) - for object in self.object_list[index+1:]: # TODO: Check effeciency + for object in self.object_list[index + 1 :]: # TODO: Check effeciency # Balls colliding with blocks if issubclass(type(object), KineticBlock) and object != self: # Do a first round pass for collision (we know object is a KineticBlock) diff --git a/src/block.py b/src/block.py index f7966e2..f3a8021 100644 --- a/src/block.py +++ b/src/block.py @@ -1,7 +1,8 @@ import pygame -from pygame.math import Vector2 -from pygame import Rect +# from pygame.math import Vector2 +# from pygame import Rect + class Block: """ @@ -12,14 +13,13 @@ def __init__(self, position, width, height, color): # Create a rectangle centered around the x and y self.position = position self.rectangle = pygame.Rect( - position.x - (width/2), - position.y - (height/2), - width, - height) + position.x - (width / 2), position.y - (height / 2), width, height + ) + self.width = width + self.height = height self.color = color self.touched_by_ball = False - def update(self, **kwargs): self.touched_by_ball = False @@ -29,9 +29,49 @@ def check_collision(self): def draw(self, screen, pygame): pygame.draw.rect(screen, self.color, self.rectangle) + class KineticBlock(Block): + """ + Constructor takes position, width, height, color + """ + # No custom code needed here, just want to be able to differentiate # KineticBall will handle the collison pass +class Paddle(KineticBlock): + """ + Class for the player paddle + """ + + def move_paddle(self, direction): + + if direction == "l" and self.position.x > self.width / 2: + self.position.x -= 3 + elif direction == "r" and self.position.x < 400 - self.width / 2: + self.position.x += 3 + self.rectangle = pygame.Rect( + self.position.x - (self.width / 2), + self.position.y - (self.height / 2), + self.width, + self.height, + ) + + +class BreakableBlock(KineticBlock): + """ + Class for blocks that can be destroyed by the paddle. + Expects RGB values of 0-63, which it will then multiply by its number of hp. + Expects 1-4 hp. + """ + + def __init__(self, position, width, height, color, hp): + self.hp = hp + self.orig_color = color + color = [i * hp for i in color] + super().__init__(position, width, height, color) + + def got_hit(self): + self.hp -= 1 + self.color = [i * self.hp for i in self.orig_color] diff --git a/src/draw.py b/src/draw.py index 14ff036..c7e4bcd 100644 --- a/src/draw.py +++ b/src/draw.py @@ -1,62 +1,123 @@ -import pygame #TODO: Fix intellisense +import pygame # TODO: Fix intellisense import random +import sys from pygame.math import Vector2 -from ball import * -from block import * +from ball import GameBall +from block import KineticBlock, Paddle, BreakableBlock -SCREEN_SIZE = [640, 480] +SCREEN_SIZE = [400, 800] BACKGROUND_COLOR = [255, 255, 255] +INVINCIBLE_PERCENT = .05 +REGULAR_PERCENT = .7 + def debug_create_objects(object_list): - kinetic = GameBall(1, object_list, SCREEN_SIZE, - Vector2(random.randint(20, SCREEN_SIZE[0] - 20), random.randint(20, SCREEN_SIZE[1] - 20)), - Vector2(4*random.random() - 2, 4*random.random() - 2), - [255, 10, 0], 20) + kinetic = GameBall( + 1, + object_list, + SCREEN_SIZE, + Vector2( + random.randint(20, SCREEN_SIZE[0] - 20), + random.randint(20, SCREEN_SIZE[1] - 20), + ), + Vector2(4 * random.random(), 4 * random.random()), + [255, 10, 0], + 10, + ) object_list.append(kinetic) - block = KineticBlock(Vector2(200,200), 100, 100, [0, 0, 255]) + block = BreakableBlock(Vector2(200, 200), 100, 100, [0, 0, 63], 3) object_list.append(block) - + paddle = Paddle(Vector2(200, 750), 100, 20, [0, 255, 0]) + object_list.append(paddle) + + +def for_reals_create_objects(object_list): + ball_vector = Vector2(random.randrange(-1000, 1000), random.randrange(-1000, 0)) + while 0 in ball_vector: + ball_vector = Vector2(random.randrange(-1000, 1000), random.randrange(-1000, 0)) + print(ball_vector) + kinetic = GameBall( + 1, + object_list, + SCREEN_SIZE, + Vector2(200, 715), + ball_vector.normalize() * 5, + [random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)], + 10, + ) + object_list.append(kinetic) + + for j in range(0, 8): + for i in range(0, 12): + die_roll = random.random() + if die_roll < INVINCIBLE_PERCENT: + new_block = KineticBlock( + Vector2(j * 50 + 25, i * 20 + 10), 50, 20, [200, 200, 200] + ) + object_list.append(new_block) + elif die_roll < INVINCIBLE_PERCENT + REGULAR_PERCENT: + new_block = BreakableBlock( + Vector2(j * 50 + 25, i * 20 + 10), + 50, + 20, + [ + random.randint(32, 63), + random.randint(32, 63), + random.randint(32, 63), + ], + random.randint(1, 4), + ) + object_list.append(new_block) + + paddle = Paddle(Vector2(200, 750), 100, 20, [0, 255, 0]) + object_list.append(paddle) + + def main(): pygame.init() screen = pygame.display.set_mode(SCREEN_SIZE) - + # Used to manage how fast the screen updates clock = pygame.time.Clock() - - object_list = [] # list of objects of all types in the toy - - debug_create_objects(object_list) - - while True: # TODO: Create more elegant condition for loop - left = False - right = False - + + object_list = [] # list of objects of all types in the toy + + for_reals_create_objects(object_list) + + while True: # TODO: Create more elegant condition for loop + direction = None + for event in pygame.event.get(): - if event.type == pygame.QUIT: sys.exit() - - #TODO: Feed input variables into update for objects that need it. + if event.type == pygame.QUIT: + sys.exit() + + # TODO: Feed input variables into update for objects that need it. keys = pygame.key.get_pressed() + if keys[pygame.K_LEFT]: - left = True + direction = "l" if keys[pygame.K_RIGHT]: - right = True + direction = "r" for object in object_list: + if isinstance(object, Paddle): + object.move_paddle(direction) object.update() object.check_collision() - + # Draw Updates screen.fill(BACKGROUND_COLOR) for ball in object_list: ball.draw(screen, pygame) - + clock.tick(60) pygame.display.flip() - + # Close everything down pygame.quit() - + + if __name__ == "__main__": main()