Jeu-GTC/game.py
2024-10-26 08:23:07 +02:00

372 lines
16 KiB
Python

# Here we import everything we need
import pygame
import math
import csv
import random
# pygame setup
pygame.init()
screen = pygame.display.set_mode((1080,720))
clock = pygame.time.Clock()
running = True
dt = 0
class Waypoint:
def __init__(self, name: str) -> None:
"""Create the corresponding waypoint according to the letter specified in name: str"""
self.name = name
match name.capitalize():
case "A":
self.x = 800
self.y = 900
self.connection = ("B", "H")
case "B":
self.x = 1575
self.y = 900
self.connection = ("A", "E", "C")
case "C":
self.x = 2500
self.y = 900
self.connection = ("B", "D")
case "D":
self.x = 2500
self.y = 1600
self.connection = ("C", "E", "F")
case "E":
self.x = 1575
self.y = 1600
self.connection = ("B", "D", "G")
case "F":
self.x = 2500
self.y = 2200
self.connection = ("D", "G")
case "G":
self.x = 1575
self.y = 2200
self.connection = ("H", "E", "F")
case "H":
self.x = 800
self.y = 2200
self.connection = ("A", "G")
def get_new_connected(self)-> object:
"""Return a new waypoint connected to the current one"""
letter = random.choice(self.connection)
return Waypoint(letter)
def get_direction(self, pointA: object, pointB: object)-> str:
"""Helper that returns the direction: str from pointA: Waypoint, to pointB: Waypoint"""
a = pointA.name
b = pointB.name
if (a == "H" and b == "A") or (a == "G" and b == "E") or (a == "E" and b == "B") or (a == "F" and b == "D") or (a == "D" and b == "C"):
return "north"
elif (a == "A" and b == "H") or (a == "E" and b == "G") or (a == "B" and b == "E") or (a == "D" and b == "F") or (a == "C" and b == "D"):
return "south"
elif (a == "A" and b == "B") or (a == "B" and b == "C") or (a == "E" and b == "D") or (a == "H" and b == "G") or (a == "G" and b == "F"):
return "east"
elif (a == "B" and b == "A") or (a == "C" and b == "B") or (a == "D" and b == "E") or (a == "G" and b == "H") or (a == "F" and b == "G"):
return "west"
else:
# If the direction is impossible
raise Exception("crash: POINT A and B impossible")
class Pnj:
def __init__(self, x: int, y: int, sprite_path: str, direction: str, objectif: Waypoint, speed: int) -> None:
"""Create a single pnj with the following attributes:
- x: int, x coordinate
- y: int, y coordinate
- sprite_path: str, the path to the sprite of the pnj
- direction: str, the direction can be "north", "south", "east", or "west"
- objectif: Waypoint, the goal of the pnj
- speed: int, the desired speed of the pnj"""
self.x = x
self.y = y
self.sprite = pygame.transform.scale_by(pygame.image.load(sprite_path), 3.0)
self.direction = direction
self.objectif = objectif
self.speed = speed
self.alive = True
def avance(self):
'''
Vérifie que l'objectif n'est pas atteint et avance le pnj dans la direction
'''
if not self.alive:
return
if self.check_objectif():
return
else:
match self.direction:
case "north":
self.y -= self.speed
case "south":
self.y += self.speed
case "east":
self.x += self.speed
case "west":
self.x -= self.speed
case _:
pass
def check_objectif(self):
"""
Vérifie que l'objectif n'est pas atteint et renvoie true si un l'est, s'occupe aussi de réassigner un nouvel objectif
"""
match self.direction:
case "north":
if self.y <= self.objectif.y:
new_objectif = self.objectif.get_new_connected()
self.direction = self.objectif.get_direction(self.objectif, new_objectif)
self.objectif = new_objectif
return True
case "south":
if self.y >= self.objectif.y:
new_objectif = self.objectif.get_new_connected()
self.direction = self.objectif.get_direction(self.objectif, new_objectif)
self.objectif = new_objectif
return True
case "east":
if self.x >= self.objectif.x:
new_objectif = self.objectif.get_new_connected()
self.direction = self.objectif.get_direction(self.objectif, new_objectif)
self.objectif = new_objectif
return True
case "west":
if self.x <= self.objectif.x:
new_objectif = self.objectif.get_new_connected()
self.direction = self.objectif.get_direction(self.objectif, new_objectif)
self.objectif = new_objectif
return True
return False
def kill(self):
"""Kill the PNJ by replacing it with a blood splatter"""
self.sprite = pygame.transform.scale_by(pygame.image.load("assets/blood.png"), 3.0)
self.alive = False
player.killcounter += 1
if player.killcounter == game.pnj_number:
village.final_boss()
elif player.killcounter >= game.pnj_number+8:
game.game_over()
# play sound effect
blood = pygame.mixer.Sound("assets/blood-splatter.mp3")
pygame.mixer.Sound.play(blood)
class Village:
def __init__(self, nb_pnj: int) -> None:
"""This class defines the village of pnj in it's globality and nb_pnj (int) determines how many pnj should be generated"""
self.liste_pnj = []
# here we generate each pnj randomly and store it in liste_pnj
for i in range(nb_pnj):
start_waypoint = Waypoint(random.choice(("A", "B", "C", "D", "E", "F", "G", "H")))
objective_waypoint = start_waypoint.get_new_connected()
self.liste_pnj.append(Pnj(start_waypoint.x, start_waypoint.y, "assets/pnj/pnj"+str(random.randint(1,8))+".png", start_waypoint.get_direction(start_waypoint, objective_waypoint), objective_waypoint, random.randint(1, 4)))
def update_pnj(self)-> None:
"""Make each pnj move a little bit"""
for p in self.liste_pnj:
p.avance()
def get_village_sprites(self)->list:
"""Method that returns a list of tuple containing the coordinates and sprite of a pnj
The tuple is in this shape: (x, y, sprite)"""
village_sprites = []
for p in self.liste_pnj:
village_sprites.append((p.x, p.y, p.sprite))
return village_sprites
def check_kill(self)->None:
"""Check for each pnj if he is being killed by the player"""
player_rect = pygame.Rect(player.x-50, player.y-50, 100, 100)
for p in self.liste_pnj:
if p.alive and player_rect.collidepoint(p.x, p.y):
p.kill()
def final_boss(self)->None:
"""Called when all original pnj where killed, it create an army of really fast knight"""
for i in ("A", "B", "C", "D", "E", "F", "G", "H"):
start_waypoint = Waypoint(i)
objective_waypoint = start_waypoint.get_new_connected()
self.liste_pnj.append(Pnj(start_waypoint.x, start_waypoint.y, "assets/pnj/knight.png", start_waypoint.get_direction(start_waypoint, objective_waypoint), objective_waypoint, 10))
class Player:
def __init__(self)-> None:
"""This class defines how the player can behave,
it should be created as soon as possible and only once in the game"""
self.x = 800
self.y = 900
self.mov_speed = 8
self.rotate_speed = 5
self.angle = 90
self.killcounter = 0
def rotate(self, angle: math.degrees)-> None:
"""Rotate the player to the desired angle"""
self.angle += angle*self.rotate_speed
def move(self, mov: int)-> None:
"""Move the player in the direction he is looking or backward if mov is negative"""
new_x = self.x + self.mov_speed*mov*math.cos(math.radians(self.angle))
new_y = self.y + self.mov_speed*mov*math.sin(math.radians(self.angle))
if(game.test_collision(pygame.Rect(new_x-50, new_y-50, 100, 100), game.collisions) == False):
self.x = new_x
self.y = new_y
elif(game.test_collision(pygame.Rect(self.x-50, new_y-50, 100, 100), game.collisions) == False):
self.y = new_y
elif(game.test_collision(pygame.Rect(new_x-50, self.y-50, 100, 100), game.collisions) == False):
self.x = new_x
village.check_kill()
class Game:
def __init__(self):
"""The main class that defines the game and it's behaviour,
it should be created only once during the game, as soon as possible"""
self.is_paused = False
self.pnj_number = 20
pygame.font.init()
self.font = pygame.font.Font('assets/upheavtt.ttf', 60)
self.load_sprites()
self.load_collisions()
def load_sprites(self):
"""Useful to load sprites in memory, should be called as soon as possible to avoid errors"""
self.perso_sprite = pygame.image.load("assets/Horse2.png")
self.map_sprite = pygame.image.load("assets/map3.png")
def load_collisions(self):
"""Load all collisions stored in the sheets located in assets/collisions.csv,
should be called as soon as possible to avoid errors"""
self.collisions = []
with open('assets/collisions.csv', 'r', newline='') as file:
reader = csv.DictReader(file)
for row in reader:
self.collisions.append(pygame.Rect(float(row["starting_point_x"]), float(row["starting_point_y"]), float(row["len_x"]), float(row["len_y"])))
def test_collision(self, objet: pygame.Rect, l_collisions: list) -> bool:
"""This is an helper to check if there is collision between an object (pygame.Rect) and a list
of collisions containing pygame.Rect"""
for col in l_collisions:
if objet.colliderect(col):
return True
return False
def display_debug_text(self):
"""This is only for debuging and should be called in display_all() if in debug mode,
but not on the final game"""
angle_surface = self.font.render("angle: "+str(player.angle), False, (0, 0, 0))
x_surface = self.font.render("x: "+str(player.x), False, (0, 0, 0))
y_surface = self.font.render("y: "+str(player.y), False, (0, 0, 0))
kill_surface = self.font.render("kills: "+str(player.killcounter), False, (0, 0, 0))
screen.blit(angle_surface, (0,0))
screen.blit(x_surface, (0,30))
screen.blit(y_surface, (0,60))
screen.blit(kill_surface, (0,90))
#pygame.draw.rect(screen, "blue",pygame.Rect(player.x-50, player.y-50, 100, 100))
def display_ui(self):
"""Handle the display of the killcounter and time,
should be called at each frames"""
color = "black"
if player.killcounter < game.pnj_number:
kill_surface = self.font.render("Kills: "+str(player.killcounter)+"\\"+str(game.pnj_number), False, color)
else:
kill_surface = self.font.render("Kills: "+str(player.killcounter)+"\\"+str(game.pnj_number+8), False, color)
screen.blit(kill_surface, (10,0))
seconds = pygame.time.get_ticks()/1000
seconds = seconds % (24 * 3600)
seconds %= 3600
minutes = seconds // 60
seconds %= 60
time_surface = self.font.render("Time: "+str(int(minutes)).zfill(2)+":"+str(int(seconds)).zfill(2), False, color)
screen.blit(time_surface, (740,0))
def check_input(self):
"""Here we check all possible user input and execut and action acordingly"""
keys = pygame.key.get_pressed()
if keys[pygame.K_z]:
_ = player.move(1)
if keys[pygame.K_s]:
_ = player.move(-1)
if keys[pygame.K_q]:
player.rotate(-1)
if keys[pygame.K_d]:
player.rotate(1)
def draw_player(self):
"""Draw the player after applying a rotozoom in the center of the screen"""
img = pygame.transform.rotozoom(self.perso_sprite, -player.angle-90, 1)
screen.blit(img, (540-img.get_rect().centerx, 360-img.get_rect().centery))
def draw_village(self):
"""Draw each sprite of the village"""
for s in village.get_village_sprites():
screen.blit(s[2], (540-player.x + s[0] - s[2].get_rect().centerx, 360-player.y + s[1] - s[2].get_rect().centery))
def display_all(self):
"""This is a helper that will render everything needed on the screen"""
# fill the screen with a color to wipe away anything from last frame and draw the map
screen.fill("gray")
screen.blit(self.map_sprite, (540-player.x, 360-player.y))
self.draw_village()
self.draw_player()
# We display them at the end so it's on top of all
#self.display_debug_text()
self.display_ui()
# flip() the display to put our work on screen
pygame.display.flip()
def game_over(self)->None:
"""Create a gameover loop displaying the scoreboard and an image"""
screen.blit(pygame.image.load("assets/gameover.png"), (0,0))
seconds = pygame.time.get_ticks()/1000
seconds = seconds % (24 * 3600)
seconds %= 3600
minutes = seconds // 60
seconds %= 60
time_surface = self.font.render(str(int(minutes)).zfill(2)+":"+str(int(seconds)).zfill(2), False, "white")
screen.blit(time_surface, (500,500))
# flip() the display to put your work on screen
pygame.display.flip()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit()
# Here we create the principal objects that will always be used during the game
player = Player()
game = Game()
village = Village(game.pnj_number)
# start music with -1 meaning infinite loop
pygame.mixer.music.load("assets/music.mp3")
pygame.mixer.music.play(-1)
# Infinite game loop
while running:
# poll for events
# pygame.QUIT event means the user clicked X on the window
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# make all the pnj move and do their pnj stuff
village.update_pnj()
# Here we check all inputs from the user and do what is requiered
game.check_input()
# Here we display everything to the screen
game.display_all()
# limits FPS to 60
# dt is delta time in seconds since last frame
dt = clock.tick(60) / 1000
# cleanly quit the pygame instance
pygame.quit()