diff --git a/Game4.py b/Game4.py new file mode 100644 index 0000000..dae9271 --- /dev/null +++ b/Game4.py @@ -0,0 +1,231 @@ +""" +Blockade is competitive arcade game. It has two players. Player 1 is a white +block that is continually moving forward. Player 2’s job is to stop Player 1 by +throwing obstacles in its way. Player 1 can avoid obstacles by jumping over or +ducking under them. Both players are limited by a stamina bar. Actions such +jumping or throwing obstacles depletes player’s stamina and if the stamina gets +too low players cannot perform actions. The game uses keyboard inputs as commands. + +@author: Vivien Chen and Harrison Young +""" + + +import os, sys +import pygame +from random import randint + +from models import * +from config import * + + +class Game4Main: + """The Main Game4 Class - This class handles the main + initialization and creating of the Game.""" + + def __init__(self, width=480,height=360): + # initialize pygame + pygame.init() + # set window size + self.width = width + self.height = height + # create screen + self.screen = pygame.display.set_mode((self.width, self.height)) + # create clock + self.clock = pygame.time.Clock() + self.score = 0 + self.oneplayer = True + + + def start_menu(self): + """Start screen for game.""" + while True: + if pygame.QUIT in {event.type for event in pygame.event.get()}: + sys.exit() + + # display start menu + self.screen.fill(colors['BLACK']) + # title + font = pygame.font.SysFont("comicsansms", int(self.width/8)) + text = font.render("Blockade", True, colors['WHITE']) + text_rect = text.get_rect(center=(self.width/2, self.height/3)) + self.screen.blit(text, text_rect) + # instructions + font = pygame.font.SysFont("comicsansms", int(self.width/30)) + text = font.render("Player 1: Arrow Keys, Player 2: ZXC", True, colors['WHITE']) + text_rect = text.get_rect(center=(self.width/2, self.height/2)) + self.screen.blit(text, text_rect) + # buttons + self.button("One Player",self.width*1/8-20,self.height*3/4,colors['GREEN'],True,self.main_loop) + self.button("Two Player",self.width*5/8-20,self.height*3/4,colors['RED'],False,self.main_loop) + + # update screen + pygame.display.flip() + + + def main_loop(self): + """Main screen for game.""" + # start count + count = 0 + + # initialize player + player = Player(PLAY_X, self.height-PLAY_LEN, PLAY_LEN, self.screen) + + # initialize stamina bars + P1_stamina_bar = StaminaBar(self.screen, P1_STAMINA_BAR_OFFSET, 'WHITE') + P2_stamina_bar = StaminaBar(self.screen, self.width - P2_STAMINA_BAR_OFFSET, 'RED') + + # initialize obstacle length, x and y coordinates + OBS_X = self.width + OBS_Y = self.height - OBS_LEN + + # create list of obstacles + if self.oneplayer: + obstacles = [Obstacle(OBS_X, OBS_Y, OBS_LEN, self.screen,'BLUE')] + else: + obstacles = [] + new_obstacle = False + obstacle_height = OBS_Y + + # initialize time variables + prev_time = 0 + obs_dt = 500 + + # main event loop + while True: + count+=1 + # if pygame.QUIT in {event.type for event in pygame.event.get()}: + # sys.exit() + # elif pygame.KEYDOWN in {event.type for event in pygame.event.get()}: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit() + if not self.oneplayer: + # check keyboard for obstacle player + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_z: + new_obstacle = True + obstacle_height = OBS_Y-LEVEL_OFFSETS['ground'] + if event.key == pygame.K_x: + new_obstacle = True + obstacle_height = OBS_Y-LEVEL_OFFSETS['first'] + if event.key == pygame.K_c: + new_obstacle = True + obstacle_height = OBS_Y-LEVEL_OFFSETS['second'] + + # check keyboard for main player + pressed = pygame.key.get_pressed() + if pressed[pygame.K_UP] and P1_stamina_bar.bars >= JUMP_COST: + if player.play_y == (self.height - player.play_len): + P1_stamina_bar.decrease_bar(JUMP_COST) + player.jump() + P1_stamina_bar.draw() + if pressed[pygame.K_LEFT]: + player.move_left() + if pressed[pygame.K_RIGHT]: + player.move_right() + + # refresh screen + self.screen.fill(colors['BLACK']) + + # update and draw player + player.update() + player.draw() + + # update current time + current_time = pygame.time.get_ticks() + + if self.oneplayer: + # generate obstacle at random time + if (current_time - prev_time > obs_dt): + obstacles.append(Obstacle(OBS_X, OBS_Y, OBS_LEN, self.screen,'GREEN')) + prev_time = current_time + obs_dt = randint(250, 1000) + if not self.oneplayer: + # generate obstacle player's obstacle at appropriate height + if (new_obstacle == True and P2_stamina_bar.bars >= OBSTACLE_COST): + new_obstacle = False + P2_stamina_bar.decrease_bar(OBSTACLE_COST) + obstacles.append(Obstacle(OBS_X, obstacle_height, OBS_LEN, self.screen,'RED')) + + # move each obstacle forward + for obstacle in obstacles: + obstacle.move_forward() + obstacle.draw() + # check for collision between player and obstacles + if player.is_collide(obstacle.obs_x, obstacle.obs_y, obstacle.obs_len): + self.game_over(str(count)) + # remove obstacle from list if off screen + obstacles = [obstacle for obstacle in obstacles if not obstacle.is_gone()] + + # update stamina bars + P1_stamina_bar.increase_bar() + P1_stamina_bar.draw() + if not self.oneplayer: + P2_stamina_bar.increase_bar(1.5) + P2_stamina_bar.draw() + + # display score + font = pygame.font.SysFont("comicsansms", int(self.width/8)) + text = font.render(str(count), True, colors['WHITE']) + text_rect = text.get_rect(center=(self.width/2, self.height/8)) + self.screen.blit(text, text_rect) + + # update screen + pygame.display.flip() + self.clock.tick(30) + + + def game_over(self, score): + """Game over screen.""" + # main event loop + while True: + if pygame.QUIT in {event.type for event in pygame.event.get()}: + sys.exit() + + # check keyboard for space key to restart game + pressed = pygame.key.get_pressed() + if pressed[pygame.K_SPACE]: + self.main_loop() + if pressed[pygame.K_v]: + self.start_menu() + + # display game over screen + self.screen.fill(colors['BLACK']) + font = pygame.font.SysFont("comicsansms", int(self.width/16)) + text = font.render("Score: " + str(score), True, colors['WHITE']) + text_rect = text.get_rect(center=(self.width/2, self.height*1/3)) + self.screen.blit(text,text_rect) + text = font.render("Press Space Bar to play again", True, colors['WHITE']) + text_rect = text.get_rect(center=(self.width/2, self.height*1/2)) + self.screen.blit(text,text_rect) + text = font.render("Press V to go to home screen", True, colors['WHITE']) + text_rect = text.get_rect(center=(self.width/2, self.height*2/3)) + self.screen.blit(text,text_rect) + + # update screen + pygame.display.flip() + + + def button(self, msg, x, y, color, oneplayer, action=None): + "Button for start screen." + mouse = pygame.mouse.get_pos() + click = pygame.mouse.get_pressed() + font = pygame.font.SysFont("comicsansms", int(self.width/20)) + text = font.render(msg, True, colors['WHITE']) + w = self.width/3 + h = self.width/12 + + if x+w > mouse[0] > x and y+h > mouse[1] > y: + pygame.draw.rect(self.screen, color, (x,y,w,h)) + + if click[0] == 1 and action != None: + self.oneplayer = oneplayer + action() + else: + pygame.draw.rect(self.screen, colors['BLACK'], (x,y,w,h)) + + self.screen.blit(text,[x+20,y]) + + +if __name__ == "__main__": + Game4Main().start_menu() diff --git a/Old Files/gameover.png b/Old Files/gameover.png new file mode 100644 index 0000000..ef7e113 Binary files /dev/null and b/Old Files/gameover.png differ diff --git a/Old Files/playermoves.py b/Old Files/playermoves.py new file mode 100644 index 0000000..75b681d --- /dev/null +++ b/Old Files/playermoves.py @@ -0,0 +1,25 @@ +import pygame +from pygame.locals import * +import time +from sys import exit +pygame.init() +def jump(ctime, startloc): + """ + Changes the y position up one frame + #for i in range(21): + + #print(jump(i,0)[1]) + """ + over = False + h = 100 + t = 20 + b = startloc + c = t/2 + a = h/((t/2)**2) + x = (ctime%20) + recty = (a*(x - c)**2)+b + + + if (x == 0): + over = True + return [recty, over] diff --git a/Old Files/pygame_base_template.py b/Old Files/pygame_base_template.py new file mode 100644 index 0000000..5eb8bcc --- /dev/null +++ b/Old Files/pygame_base_template.py @@ -0,0 +1,115 @@ +""" + Pygame base template for opening a window + + Sample Python/Pygame Programs + Simpson College Computer Science + http://programarcadegames.com/ + http://simpson.edu/computer-science/ + + Explanation video: http://youtu.be/vRB_983kUMc +""" + + + +import pygame +from playermoves import * + + +# Define some colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GREEN = (0, 255, 0) +RED = (255, 0, 0) +jumping = False +pygame.init() + +# Set the width and height of the screen [width, height] +size = (700, 500) +screen = pygame.display.set_mode(size) + +pygame.display.set_caption("My Game") + +# Loop until the user clicks the close button. +done = False + +# Used to manage how fast the screen updates +clock = pygame.time.Clock() +frames = 0; +startjump = 0 +x_min = 0 +x_max = 200 +rectx = 100 +recty = 400 + +# -------- Main Program Loop ----------- +while not done: + + # --- Main event loop + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + done = True + #if event.type == pygame.KEYDOWN: +# + # if event.key == pygame.K_SPACE: + # if frames - startjump > 20: + # jumping = True + # startjump = frames + + pressed = pygame.key.get_pressed() + if pressed[pygame.K_UP]: + if frames - startjump > 20: + jumping = True + startjump = frames + if pressed[pygame.K_LEFT]: + if rectx > x_min: + rectx -= 5 + if pressed[pygame.K_RIGHT]: + if rectx < x_max: + rectx += 5 + + + + + + # --- Game logic should go here + + # --- Screen-clearing code goes here + + # Here, we clear the screen to white. Don't put other drawing commands + # above this, or they will be erased with this command. + + # If you want a background image, replace this clear with blit'ing the + # background image. + screen.fill(BLACK) + pygame.draw.rect(screen, RED, [0,425,700,100]) + jumpclock = frames%21 + + # --- Drawing code should go here + # --- Drawing code should go here + + jumpover = False + + #if False:#if detect_key_press() == 'space': + # jumping = True #if the spacebar is pressed start the jump + # print("boing") + #else: + # print(detect_key_press()) + if jumping == True: + recty = jump(frames-startjump+1,300)[0] + jumpover = jump(frames-startjump+1,300)[1] + if jumpover == True: + + jumping = False + + pygame.draw.rect(screen, WHITE, [rectx,recty,25,25]) + # --- Go ahead and update the screen with what we've drawn + pygame.display.flip() + frames+=1 # increments frame count + + # --- Limit to 60 frames per second + clock.tick(60) + + +# Close the window and quit. +pygame.quit() diff --git a/Project_Writeup_and_Reflection.md b/Project_Writeup_and_Reflection.md new file mode 100644 index 0000000..7be4f5d --- /dev/null +++ b/Project_Writeup_and_Reflection.md @@ -0,0 +1,58 @@ +# Project Writeup and Reflection + +### Project Overview [Maximum 100 words] + +*Write a short abstract describing your project.* + +Blockade is competitive arcade game. It has two players. Player 1 is a white block that is continually moving forward. Player 2’s job is to stop Player 1 by throwing obstacles in its way. Player 1 can avoid obstacles by jumping over or ducking under them. Both players are limited by a stamina bar. Actions such jumping or throwing obstacles depletes player’s stamina and if the stamina gets too low players cannot perform actions. The game uses keyboard inputs as commands. + +### Results [~2-3 paragraphs + figures/examples] + +*Present what you accomplished. This will be different for each project, but screenshots are likely to be helpful.* + +The game starts with Player 1 on the bottom left corner of the screen. Player 1’s objective is simple, avoid obstacles and get as far as you can. Player 1 can avoid obstacles by moving left, right or jumping. These actions are performed by pressing the Left, Right, and Up arrow keys respectively. Player 2 does not show up on screen but can make is presence known by throwing obstacles at Player 1. These obstacles can be thrown at three heights: ground level, just over Player 1 and High in the air. Player 2 can control the height of the obstacles thrown by pressing the Z, X, and C keys. The game ends when Player 1 collides with an obstacle and dies, Player 1’s score is determined by how long they survived. The competitive nature of the game come when the human players swap roles. With both trying to ensure the other gets a lower score. + +Player actions cost stamina. If a player’s stamina gets too low, they cannot perform any actions until it regenerates. This adds another level of strategy to the game with each player trying to force the other to deplete their stamina bar. In the game, Player 2’s strategy heavily relies on forcing Player 1 to run out of stamina. + +We playtested the game with around 9 individuals. Each person played at least one game as both players. Reviews were generally positive with testers describing it as "fascinating" and "a few minutes of mindless fun". + + +![screenshot1](https://github.com/vivienyuwenchen/InteractiveProgramming/blob/master/screenshot1.png) + +Figure 1: This is how the game starts. Player 1 is the white square in the bottom left corner. PLayer 1’s distance is in the top center of the screen. The stamina bars for Player 1 and 2 are in the top left and right screen respectively. + +![screenshot2](https://github.com/vivienyuwenchen/InteractiveProgramming/blob/master/screenshot2.png) + +Figure 2: During the game Player 1 avoids the red obstacles by jumping. Player 2 can spawn red obstacles at multiple heights. Player 2’s staminal bar (red) has almost been depleted. + +![screenshot3](https://github.com/vivienyuwenchen/InteractiveProgramming/blob/master/screenshot3.png) + +Figure 3: Game over screen reports the distance Player 1 traveled and gives instructions for how to play again. + +### Implementation [~2-3 paragraphs + UML diagram] + +*Describe your implementation at a system architecture level. Include a UML class diagram, and talk about the major components, algorithms, data structures and how they fit together. You should also discuss at least one design decision where you had to choose between multiple alternatives, and explain why you made the choice you did.* + +Our minimum viable product was a one player game similar to Google Chrome's dino run, in which the player jumps over randomly generated obstacles. The first class we created was for the main game. Its init handled the main initialization of the game. The mainLoop was a method that ran the main loop of the game, which got keyboard inputs and used them to control the player (and later the obstacles). We created two other classes. Unsurprisingly, they were the player and the obstacle. The obstacle only needed to move forward, so it had a moveForward method aside from init and repr, as well as isGone, used to delete obstacles off screen to prevent the programming from crashing from having to keep track of too many obstacles. The player had more requirements, such as moving left, moving right, and jumping, so we created the methods accordingly. We also added a method for collision detection in order to determine when game over should happen. We then created a gameOver method inside the main class game. We included a conditional statement in the mainLoop to direct the program to gameOver when the player collides with any obstacle and a conditional in gameOver to direct the program back to mainLoop when the space key is pressed. + +We got our MVP done by the mid-project check-in, so we added additional features to allow a second player to control the obstacles. We then created another class for the stamina bar, with one method to decrease the bar with every action (i.e. when the player jumps for the player's stamina bar and when an obstacle is generated for the second player's stamina bar) and increase the bar over time up to a certain length. + +![UML](https://github.com/vivienyuwenchen/InteractiveProgramming/blob/master/UML.png) + +Figure 4: UML diagram of game + +At one point, we considered implementing Model View Control. However, given the simplicity of our game, it wasn't worth rewriting whole sections just to split up the view and control into different classes. It would have cut down on the length of our mainLoop, but the pros just didn't outweigh the cons. Another decision that we made was to not create a subclass for each of the stamina bars since we only needed two and their only differences were their color and location, so we took those parameters as arguments instead. We also kept remnants of the code that randomized obstacles from our MVP in case we wanted to revise the code to allow the player choose their mode: one-player or two-player. + +### Reflection [~2 paragraphs] + +*From a process point of view, what went well? What could you improve? Other possible reflection topics: Was your project appropriately scoped? Did you have a good plan for unit testing? How will you use what you learned going forward? What do you wish you knew before you started that would have helped you succeed?* + +*Also discuss your team process in your reflection. How did you plan to divide the work (e.g. split by class, always pair program together, etc.) and how did it actually happen? Were there any issues that arose while working together, and how did you address them? What would you do differently next time?* + +Overall, this project went pretty well. We were able to deliver our MVP as well as a few of our stretch goals, like allowing a second player to generate obstacles and adding stamina bars, a scorekeeper, and a display of the score on the game over screen. For the most part, we planned on and were able to divide up the work based on what we wanted to complete within a period of time. We didn't look that far ahead, but planned at the beginning of each of our meetings what we wanted accomplished and divided the work as evenly as possible. Harry developed the main player and Vivien developed the obstacles. Once we made the two players, we both fixed bugs as they came up. + +One problem we ran into was keeping a consistent class format. We developed the two players independently and as a result each player was done a little differently. We refactored the main player into an object in order to keep them consistent with each other. For the most part, we were able to effectively work independently and met up to either discuss future features for the project or how to fix bugs. + +For a project of this scope our teamwork strategy was appropriate. However, it is not scalable. For larger projects we should spend more time planning out the classes so they can be easily integrated. We also tried to implement a CMV structure into the game; however, by that point most of the game had been written and changing the structure was deemed not worth the effort. If we were to do this project again we would have implement CMV from the beginning. + +Unfortunately, Vivien's family came to visit the weekend before the project was due, so she could not spend as much time adding more features to the game as Harry really wanted to do. However, since we got quite a lot done early on, Vivien was able to spend time with her family and Harry just added some of the things he wanted to add. diff --git a/README.md b/README.md index 825cba1..37d5e73 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # InteractiveProgramming This is the base repo for the interactive programming project for Software Design at Olin College. + +[Project Writeup and Reflection](https://github.com/vivienyuwenchen/InteractiveProgramming/blob/master/Project_Writeup_and_Reflection.md) + +Required packages: +- pip install pygame + +To run the game: +- python Game4.py diff --git a/UML.png b/UML.png new file mode 100644 index 0000000..85fbb66 Binary files /dev/null and b/UML.png differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..6190651 --- /dev/null +++ b/config.py @@ -0,0 +1,21 @@ +colors = {'BLACK': (0, 0, 0), + 'WHITE' : (255, 255, 255), + 'GREEN' : (0, 255, 0), + 'RED' : (255, 0, 0), + 'BLUE' : (0, 0, 255), + } + +PLAY_LEN = 25 +PLAY_X = 100 + +P1_STAMINA_BAR_OFFSET = 25 +P2_STAMINA_BAR_OFFSET = 125 +JUMP_COST = 33 +OBSTACLE_COST = 33 + +OBS_LEN = 25 + +LEVEL_OFFSETS = {'ground': 0, + 'first': 25, + 'second': 50, + } diff --git a/models.py b/models.py new file mode 100644 index 0000000..1f16a25 --- /dev/null +++ b/models.py @@ -0,0 +1,124 @@ +import os, sys +import pygame + +from config import * + + +class Obstacle: + """A square obstacle, defined by its top left hand coordinate, length, and color. + Also takes in screen as an argument to draw the obstacle.""" + def __init__(self, obs_x, obs_y, obs_len, screen, color): + """Initialize the instance.""" + self.obs_x = obs_x # top left hand x coordinate + self.obs_y = obs_y # top left hand y coordinate + self.obs_len = obs_len # side length + self.screen = screen # game screen + self.color = color # color of obstacle + + def __repr__(self): + return 'Obstacle({}, {}, {}, {})'.format(self.obs_x, self.obs_y, self.obs_len, self.screen) + + def draw(self): + """Draw osbstacle based on top left hand coordinate and length.""" + pygame.draw.rect(self.screen, colors[self.color], [self.obs_x, self.obs_y, self.obs_len, self.obs_len]) + + def move_forward(self): + """Update horizontal location of obstacle.""" + self.obs_x -= 20 + + def is_gone(self): + """Check if obstacle is completely off screen.""" + return self.obs_x < -self.obs_len + + +class Player: + """A square player, defined by its top left hand coordinate and length. + Also takes in screen as an argument to draw the player.""" + def __init__(self, play_x, play_y, play_len, screen): + """Initialize the instance.""" + self.play_x = play_x # top left hand x coordinate + self.play_y = play_y # top left hand y coordinate + self.play_len = play_len # side length + self.screen = screen # game screen + self.speed = 10 # right/left speed + self.jumpInProgress = False # initialize to False + self.v = 7.5 # "velocity" for jump + self.m = 2.5 # "mass" for jump + self.floor = play_y # location of player before jump, used for comparison + + def draw(self): + """Draw player based on top left hand coordinate and length.""" + pygame.draw.rect(self.screen, colors['WHITE'], [self.play_x, self.play_y, self.play_len, self.play_len]) + + def move_right(self): + """Update horizontal location of player after moving right.""" + if self.play_x < 300: + self.play_x += self.speed + + def move_left(self): + """Update horizontal location of player after moving left.""" + if self.play_x > 0: + self.play_x -= self.speed + + def jump(self): + """Set jumping status.""" + self.jumpInProgress = True + + def update(self): + """Update height of player during jump.""" + if self.jumpInProgress: + # change in height = "mass" times "velocity" + dy = self.m * self.v + + # subtract height by change in height + self.play_y -= dy + # decrease velocity + self.v -= .75 + + # stop jumping if player has landed + if self.play_y >= self.floor: + # prevent player from falling through the floor + self.play_y = self.floor + # no longer jumping + self.jumpInProgress = False + # reset velocity + self.v = 7.5 + + def is_collide(self, obs_x, obs_y, obs_len): + """Check collision of player with obstacle.""" + # set coordinates for top left hand corner (0) and bottom right hand corner (1) of obstacle + obs_x0 = obs_x + obs_x1 = obs_x + obs_len + obs_y0 = obs_y + obs_y1 = obs_y + obs_len + # and of player + play_x0 = self.play_x + play_x1 = self.play_x + self.play_len + play_y0 = self.play_y + play_y1 = self.play_y + self.play_len + # check if player coordinates within obstacle coordinates + if (obs_x0 <= play_x0 <= obs_x1 or obs_x0 <= play_x1 <= obs_x1) and (obs_y0 <= play_y0 < obs_y1 or obs_y0 < play_y1 <= obs_y1): + return True + + +class StaminaBar: + """A stamina bar, defined by its starting location and color. + Also takes in screen as an argument to draw the stamina bar.""" + def __init__(self, screen, start, color): + self.screen = screen # game screen + self.start = start # starting location of stamina bar + self.color = color # color of stamina bar + self.bars = 100 # initialize number of health bars + + def draw(self): + """Draw stamina bar based on color, starting location, and number of health bars.""" + pygame.draw.rect(self.screen, colors[self.color], [self.start, 20, self.bars, 10]) + + def decrease_bar(self, num_bars): + """Decrease health bar by num_bars.""" + self.bars -= num_bars + + def increase_bar(self, speed = 1): + """Increase health bar continuously if number of bars is lower than 100.""" + if self.bars < 100: + self.bars += 1 * speed diff --git a/screenshot1.png b/screenshot1.png new file mode 100644 index 0000000..ee37f1a Binary files /dev/null and b/screenshot1.png differ diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 0000000..104916f Binary files /dev/null and b/screenshot2.png differ diff --git a/screenshot3.png b/screenshot3.png new file mode 100644 index 0000000..1a340f9 Binary files /dev/null and b/screenshot3.png differ