~ Game Design


Final Solution

Well done! You've gone through all the steps to create a fully functional game in python and pygame, complete with classes, a menu screen, enemies, friendly sprites, reading and writing from a file (high scores) and a next level feature. This hopefully will give you an idea of how things work at a very basic level. You could now devise your very own unique idea and start to develop it using some of these key concepts.

FINAL SOLUTION : 

The code below is the answer/solution for Challenge 6 and introduces the idea of levels. Please feel free to test the game, change variables, and then consider further extensions!

CODE: 

import pygame
import random
import math
import sys
import os

#FILE 6: Next Level
#Added variables and cases in the main loop to be able to get the next level functionality

SCREEN_WIDTH = 400
SCREEN_HEIGHT = 300


class Ball:
        def __init__(self, x, y, radius, color, screen):
                self.x = x
                self.y = y
                self.radius = radius
                self.screen = screen
                self.color = color

        def draw(self):
                pygame.draw.circle(screen, self.color, [self.x, self.y], self.radius)

class PlayerBall(Ball):
        def __init__(self, x, y, radius, color, screen):
                Ball.__init__(self, x, y, radius, color, screen)
                self.green_cooldown = 0
                self.red_cooldown = 0

        def move(self, mv_type):
                if mv_type == "UP":
                        self.y -= 5
                elif mv_type == "DOWN":
                        self.y += 5
                elif mv_type == "LEFT":
                        self.x -= 5
                elif mv_type == "RIGHT":
                        self.x += 5
                if self.x - self.radius < 0:
                        self.x = self.radius
                elif self.x + self.radius > SCREEN_WIDTH:
                        self.x = SCREEN_WIDTH - self.radius
                if self.y - self.radius < 0:
                        self.y = self.radius
                elif self.y + self.radius > SCREEN_HEIGHT:
                        self.y = SCREEN_HEIGHT - self.radius

        #Modified the check_contact method to be more general
        def check_contact(self, ball, isGreen):
                to_return = 0
                if math.sqrt((self.y - ball.y) ** 2 + (self.x - ball.x) ** 2) < self.radius + ball.radius:
                        if isGreen and self.green_cooldown == 0:
                                self.green_cooldown = 10
                                to_return += 10
                        elif not isGreen and self.red_cooldown == 0:
                                self.red_cooldown = 10
                                to_return -= 10
                return to_return

class GreenBall(Ball):
        def __init__(self, x, y, radius, color, screen):
                Ball.__init__(self, x, y, radius, color, screen)
                self.vy = random.randint(0, 4) - 2
                self.vx = random.randint(0, 4) - 2
                while self.vy == 0 or self.vx == 0:
                        self.vy = random.randint(0, 4) - 2
                        self.vx = random.randint(0, 4) - 2

        def move(self):
                self.x += self.vx
                self.y += self.vy

                if self.x - self.radius < 0:
                        self.x = self.radius
                        self.vx *= -1
                elif self.x + self.radius > SCREEN_WIDTH:
                        self.x = SCREEN_WIDTH - self.radius
                        self.vx *= -1
                if self.y - self.radius < 0:
                        self.y = self.radius
                        self.vy *= -1
                elif self.y + self.radius > SCREEN_HEIGHT:
                        self.y = SCREEN_HEIGHT - self.radius
                        self.vy *= -1

class RedBall(Ball):
        def __init__(self, x, y, radius, color, screen):
                Ball.__init__(self, x, y, radius, color, screen)
                self.vy = random.randint(0, 6) - 3
                self.vx = random.randint(0, 6) - 3
                while self.vy == 0 or self.vx == 0:
                        self.vy = random.randint(0, 6) - 3
                        self.vx = random.randint(0, 6) - 3

        def move(self):
                self.x += self.vx
                self.y += self.vy

                if self.x - self.radius < 0:
                        self.x = self.radius
                        self.vx *= -1
                elif self.x + self.radius > SCREEN_WIDTH:
                        self.x = SCREEN_WIDTH - self.radius
                        self.vx *= -1
                if self.y - self.radius < 0:
                        self.y = self.radius
                        self.vy *= -1
                elif self.y + self.radius > SCREEN_HEIGHT:
                        self.y = SCREEN_HEIGHT - self.radius
                        self.vy *= -1

class SpecialBullet(pygame.sprite.Sprite):
    def __init__(self,x,y,xSpeed,ySpeed,sprite,horizontal,scale):
        super(SpecialBullet,self).__init__()
        self.xSpeed = xSpeed
        self.ySpeed = ySpeed

        self.image = pygame.image.load(sprite).convert_alpha()
        w,h = self.image.get_size()
        self.image = pygame.transform.scale(self.image,(int(w*scale),int(h*scale)))

        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

        self.horizontal = horizontal
        self.posIntervals = []
        self.timeIntervals = []
        self.ingame = False
        self.currentInterval = 0

    def isInScreen(self):
        return self.rect.x >= 0 and self.rect.x <= SCREEN_WIDTH and self.rect.y >= 0 and self.rect.y <= SCREEN_HEIGHT

    def collidedWithPlayer(self,ball):
        res = (ball.x - ball.radius) < self.rect.x + self.rect.width
        res = res and ball.x + ball.radius > self.rect.x
        res = res and (ball.y - ball.radius) < self.rect.y + self.rect.height
        return res and ball.y + ball.radius > self.rect.y

    def update(self, timePassed):
        if self.currentInterval == 3:
            return
        if not self.ingame:
            if timePassed >= self.timeIntervals[self.currentInterval]:
                self.ingame = True
                if self.horizontal:
                    self.rect.y = self.posIntervals[self.currentInterval]
                    self.rect.x = 0
                else:
                    self.rect.x = self.posIntervals[self.currentInterval]
                    self.rect.y = 0
        else:
            self.rect.x += self.xSpeed
            self.rect.y += self.ySpeed
            if not self.isInScreen():
                self.ingame = False
                self.currentInterval += 1

    def reset(self):
        self.currentInterval = 0
        self.ingame = False

    def setIntervals(self, finishTime):
        if self.horizontal:
            self.posIntervals = [random.randint(0,SCREEN_HEIGHT), random.randint(0,SCREEN_HEIGHT), random.randint(0,SCREEN_HEIGHT)]
        else:
            self.posIntervals = [random.randint(0,SCREEN_WIDTH), random.randint(0,SCREEN_WIDTH), random.randint(0,SCREEN_WIDTH)]

        fragment = finishTime // 3
        t1 = random.randint(0,fragment)
        t2 = random.randint(fragment, fragment*2)
        t3 = random.randint(fragment*2,finishTime)
        self.timeIntervals = [t1,t2,t3]


class Angel(SpecialBullet):
    def __init__(self,x,y):
        super(Angel,self).__init__(x,y,0,4,"angel.png",False, 0.7)

#Class to make a specific SpecialBullet to represent a devil
class Devil(SpecialBullet):
    def __init__(self,x,y):
        super(Devil,self).__init__(x,y,4,0,"devil.png",True,1)

def renderMenu():
    f = pygame.font.SysFont("monospace", 30)
    title = f.render("Main Menu", 1, (0,0,0))
    screen.blit(title, (SCREEN_WIDTH//4,0))
    play = f.render("P - Play", 1, (0,0,0))
    screen.blit(play, (0,50))
    quit = f.render("Q - Quit", 1, (0,0,0))
    screen.blit(quit, (0,100))
    save = f.render("S - Save Scores", 1, (0,0,0))
    screen.blit(save, (0,150))
    view = f.render("V - View Scores", 1, (0,0,0))
    screen.blit(view, (0,200))

    res = 0
        
    pressed = pygame.key.get_pressed()
    if pressed[pygame.K_p]:
        res = 1
    if pressed[pygame.K_q]:
        res = 2
    if pressed[pygame.K_s]:
        res = 3
    if pressed[pygame.K_v]:
        res = 4
        
    pygame.display.flip()
    clock.tick(60)
    return res

def updateScores(highScores, score):
    for i in range(len(highScores)):
         if(score[1] > highScores[i][1]):
             highScores = highScores[0:i] + [score] + highScores[i:-1]
             break
    return highScores 

def saveScores(highScores):
   f = open("score.txt","w")
   for score in highScores:
       f.write(score[0] + " " + str(score[1]) + "\n")
   f.close()

def readScores():
   if os.path.isfile("score.txt"):
       f = open("score.txt","r")
       highScores = []
       for line in f:
          l = line.split(" ")
          highScores.append((l[0],int(l[1])))
       f.close()
       return highScores
   else:
       return [("Player",0),("Player",0), ("Player",0)]

def scoreSavedScreen(won):
    screen.fill((255, 255, 255))
    text = "Scores Saved!"
    text2 = ""
    if not won:
        text = "You must have won"
        text2 = "to save scores!"

    f = pygame.font.SysFont("monospace", 30)
    title = f.render(text, 1, (0,0,0))
    screen.blit(title, (0,50))
    secondLine = f.render(text2,1,(0,0,0))
    screen.blit(secondLine, (0,100))
    pygame.display.flip()
    pygame.time.delay(2000)

def viewScoresScreen():
    screen.fill((255, 255, 255))

    hScores = readScores()
    s1 = hScores[0][0] + ": " + str(hScores[0][1])
    s2 = hScores[1][0] + ": " + str(hScores[1][1])
    s3 = hScores[2][0] + ": " + str(hScores[2][1])

    f = pygame.font.SysFont("monospace", 30)
    s1Label = f.render(s1, 1, (0,0,0))
    screen.blit(s1Label, (0,50))
    s2Label = f.render(s2, 1, (0,0,0))
    screen.blit(s2Label, (0,100))
    s3Label = f.render(s3, 1, (0,0,0))
    screen.blit(s3Label, (0,150))
    
    pygame.display.flip()
    pygame.time.delay(2000)

sys.stdout.write("Please enter your name: ")
name = input()

pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
done = False
score = 0

myfont = pygame.font.SysFont("monospace", 15)

clock = pygame.time.Clock()

ball1 = PlayerBall(100, 100, 20, (0, 0, 0), screen)
ball2 = GreenBall(200, 200, 5, (0, 255, 0), screen)
ball3 = RedBall(250, 300, 90, (255, 0, 0), screen)

#The two medium sized balls for the next level
ball4 = RedBall(100, 300, 45, (255, 0, 0), screen)
ball5 = RedBall(300, 100, 45, (255, 0, 0), screen)


menuChoice = 0

timeToEnd = 20000
timeEllapsed = 0

won = False

highScores = readScores()

angelSprite = pygame.sprite.Group()
angel = Angel(0,0)
angelSprite.add(angel)

devilSprite = pygame.sprite.Group()
devil = Devil(0,0)
devilSprite.add(devil)

#Set up background for next level
bg = pygame.sprite.Sprite()
bg.image = pygame.image.load("bg.jpg").convert_alpha()
bg.rect = bg.image.get_rect()
bg.rect.x = 0
bg.rect.y = 0
bgSprite = pygame.sprite.Group()
bgSprite.add(bg)

#Variable to determine if we are in the next level
nextLevel = False

#Variables to determine the goal scores
goalScore = 100
nextLevelScore = 200

while not done:
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                        done = True
        
        screen.fill((255, 255, 255))

        if menuChoice != 1:
                menuChoice = renderMenu()
                if menuChoice == 1:
                    score = 0
                    goalScore = 100
                    nextLevelScore = 200
                    won = False
                    timeEllapsed = 0
                    angel.setIntervals(timeToEnd)
                    devil.setIntervals(timeToEnd)
                elif menuChoice == 2:
                    done = True
                elif menuChoice == 3:
                    if won:
                        saveScores(highScores)
                    scoreSavedScreen(won)
                elif menuChoice == 4:
                    viewScoresScreen()
                continue

        #Draw the clouds background if we are in the next level
        if nextLevel:
            bgSprite.draw(screen)

        #Add the nextLevelScore as a termination condition
        if score > -100 and score < nextLevelScore and timeEllapsed < timeToEnd:
                angel.update(timeEllapsed)
                devil.update(timeEllapsed)

                pressed = pygame.key.get_pressed()
                if pressed[pygame.K_UP]:
                        ball1.move("UP")
                if pressed[pygame.K_DOWN]:
                        ball1.move("DOWN")
                if pressed[pygame.K_LEFT]:
                        ball1.move("LEFT")
                if pressed[pygame.K_RIGHT]:
                        ball1.move("RIGHT")

                label = myfont.render("SCORE: " + str(score), 1, (0,0,0))
                timeLabel = myfont.render("TIME: " + str((timeToEnd - timeEllapsed) // 1000 + 1), 1,(0,0,0))
                screen.blit(label, (10, SCREEN_HEIGHT - 20))
                screen.blit(timeLabel, (300, SCREEN_HEIGHT - 20))

                ball2.move()
                score += ball1.check_contact(ball2, True)
                ball2.draw()

                #Move, check and draw the appropiate balls depending on level
                if nextLevel:
                    ball4.move()
                    score += ball1.check_contact(ball4,False)
                    ball4.draw()
                    ball5.move()
                    score += ball1.check_contact(ball5,False)
                    ball5.draw()
                else:
                    ball3.move()
                    score += ball1.check_contact(ball3,False)
                    ball3.draw()
                    
                ball1.draw()


                if ball1.green_cooldown > 0:
                        ball1.green_cooldown -= 1
                if ball1.red_cooldown > 0:
                        ball1.red_cooldown -= 1

                if angel.ingame:
                    angelSprite.draw(screen)
                    if angel.collidedWithPlayer(ball1):
                        score += 100
                        angel.ingame = False
                        angel.currentInterval += 1

                if devil.ingame:
                    devilSprite.draw(screen)
                    if devil.collidedWithPlayer(ball1):
                        score -= 100
                        devil.ingame = False
                        devil.currentInterval += 1

                timeEllapsed += clock.get_time()
        else:
                finalFont = pygame.font.SysFont("monospace", 50)
                angel.reset()
                devil.reset()
                if score < goalScore:
                        label = finalFont.render("YOU LOSE!", 1, (0,0,0))
                        screen.blit(label, (0,0))
                        menuChoice = 0
                else:
                        label = finalFont.render("YOU WIN!", 1, (0,0,0))

                        #Determine where to go after winning
                        #If we were already at the next level, go back to the main menu
                        if nextLevel:
                            won = True
                            menuChoice = 0
                            nextLevel = False
                        #If we were in the first level and we reached the score needed to get to the next level, go there.
                        #Also set up all starting values accordingly to start the level
                        elif score >= nextLevelScore:
                            menuChoice = 1
                            nextLevel = True
                            score = 200
                            goalScore = 300
                            nextLevelScore = 300
                            timeEllapsed = 0
                            angel.setIntervals(timeToEnd)
                            devil.setIntervals(timeToEnd)
                        #If we won but we didn't reach the next level, go back to the main menu
                        else:
                            menuChoice = 0
                            nextLevel = False
                            
                        highScores = updateScores(highScores,(name,score))
                        screen.blit(label, (0,0))
                
                pygame.display.flip()
                clock.tick(60)
                pygame.time.wait(3000)
                continue

        pygame.display.flip()
        clock.tick(60)

pygame.quit()