YAP/levelstate.py
2026-10-04 20:19:27 +02:00

349 lines
No EOL
13 KiB
Python

# libraries
import pygame
import numpy as np
import json
# project modules
from constants import *
from player import *
from new_gui import *
# -------------------------------------------------------------------------------
# Enum and Tables
# -------------------------------------------------------------------------------
class TileTypes(Enum):
FLOOR = '.'
WALL = '#'
RED_PLATE = 'r'
RED_DOOR = 'R'
ORANGE_PLATE = 'o'
ORANGE_DOOR = 'O'
TEAL_PLATE = 't'
TEAL_DOOR = 'T'
BLUE_ARRIVE = 'B'
GREEN_ARRIVE = 'G'
TILE_FILES = {
TileTypes.FLOOR: "assets/floor.svg",
TileTypes.WALL : "assets/wall.svg",
TileTypes.RED_PLATE : "assets/red_press.svg",
TileTypes.RED_DOOR : "assets/red_door.svg",
TileTypes.ORANGE_PLATE : "assets/orange_press.svg",
TileTypes.ORANGE_DOOR : "assets/orange_door.svg",
TileTypes.TEAL_PLATE : "assets/teal_press.svg",
TileTypes.TEAL_DOOR : "assets/teal_door.svg",
TileTypes.BLUE_ARRIVE : "assets/blue_arrive.svg",
TileTypes.GREEN_ARRIVE : "assets/green_arrive.svg"
}
class GamePanel(Enum):
BLUE_PANE = 0
BLUE_TEXT = 1
GREEN_TEXT = 2
GREEN_PANE = 3
TIMER = 4
PANEL_FILES = {
GamePanel.BLUE_PANE: "assets/blue_pane.png",
GamePanel.BLUE_TEXT: "assets/blue_text.png",
GamePanel.GREEN_PANE: "assets/green_pane.png",
GamePanel.GREEN_TEXT: "assets/green_text.png",
GamePanel.TIMER: "assets/timer.png"
}
class Direction(Enum):
HORIZ = 0
VERT = 1
# -------------------------------------------------------------------------------
# Class Level
# - represents the static data of the current level being played
# -------------------------------------------------------------------------------
class Level:
def __init__(self, path):
# Saving the order number of the level
self.level_number = int(path.name.split("_")[0])
# Level data file loading
with open(path, "r") as file:
level_data = json.load(file)
self.map = np.array(level_data["map"])
self.id = level_data["id"]
# LOADING TILE IMAGES
target_size = (TILE_SIZE, TILE_SIZE)
self.floor_tile = self.load_tile(TileTypes.FLOOR ,target_size)
self.wall_tile = self.load_tile(TileTypes.WALL ,target_size)
self.red_press = self.load_tile(TileTypes.RED_PLATE ,target_size)
self.red_door = self.load_tile(TileTypes.RED_DOOR ,target_size)
self.orange_press = self.load_tile(TileTypes.ORANGE_PLATE ,target_size)
self.orange_door = self.load_tile(TileTypes.ORANGE_DOOR ,target_size)
self.teal_press = self.load_tile(TileTypes.TEAL_PLATE ,target_size)
self.teal_door = self.load_tile(TileTypes.TEAL_DOOR ,target_size)
self.blue_arrive = self.load_tile(TileTypes.BLUE_ARRIVE ,target_size)
self.green_arrive = self.load_tile(TileTypes.GREEN_ARRIVE ,target_size)
self.doors, self.plates = self.load_objs()
# For now this method does this with the map, in the future they will be encoded differently in the json
# maybe already in arrays to make everything easier
def load_objs(self):
doors = {}
plates = {}
for (row, col), character in np.ndenumerate(self.map):
if character in ('R', 'T', 'O'):
# Todo: choose type based on character
doors[character] = Door(col, row, TileTypes(character))
if character.lower() in plates:
plates[character.lower()].set_door(doors[character])
if character in ('r', 't', 'o'):
# Todo: choose type based on character
plates[character] = PressurePlate(col, row, TileTypes(character))
if character.upper() in doors:
plates[character].set_door(doors[character.upper()])
return doors, plates
def load_tile(self, type, target_size):
return pygame.transform.smoothscale(pygame.image.load(TILE_FILES.get(type, TILE_FILES[TileTypes.FLOOR])).convert_alpha(), target_size)
def draw(self, screen, pressing):
for (row, col), character in np.ndenumerate(self.map):
# Convert grid coordinates to pixel coordinates
x = col * TILE_SIZE + X_OFFSET
y = row * TILE_SIZE + Y_OFFSET
# Create the rectangle boundary for the tile
rect = pygame.Rect(x, y, TILE_SIZE, TILE_SIZE)
# Draw the appropriate tile color
if character == '#':
screen.blit(self.wall_tile, rect)
elif character == 'B':
screen.blit(self.blue_arrive, rect)
elif character == 'G':
screen.blit(self.green_arrive, rect)
else:
screen.blit(self.floor_tile, rect)
for element in list(self.doors.values()) + list(self.plates.values()):
element.draw(screen)
# -------------------------------------------------------------------------------
# Class LevelState
# - represents the state of the current level being played
# - it manages all the gameplay logic and mechanics
# -------------------------------------------------------------------------------
class LevelState:
def __init__(self, level):
self.level = level
# Players setup
b_rows, b_cols = np.where(level.map == 'b')
g_rows, g_cols = np.where(level.map == 'g')
blue_y, blue_x = b_rows[0], b_cols[0]
green_y, green_x = g_rows[0], g_cols[0]
self.players = [Player(blue_x, blue_y, True, PlayerTypes.BLUE), Player(green_x, green_y, False, PlayerTypes.GREEN)]
self.curr_player_i = 0
self.player = self.players[self.curr_player_i]
# Sprite group for ease
self.all_sprites = pygame.sprite.Group()
self.all_sprites.add(self.players)
# state variables
self.pressing = "none"
self.blue_arrive = False
self.green_arrive = False
# Completion and final time
self.completed = False
self.final_time = 0
# GUI ELEMENTS
self.blue_pane = self.load_gui(GamePanel.BLUE_PANE)
self.green_pane = self.load_gui(GamePanel.GREEN_PANE)
self.blue_text = self.load_gui(GamePanel.BLUE_TEXT)
self.green_text = self.load_gui(GamePanel.GREEN_TEXT)
self.timer = self.load_gui(GamePanel.TIMER)
def load_gui(self, element):
return pygame.image.load(PANEL_FILES.get(element, None)).convert_alpha()
def change_player(self):
self.player.active = False
self.curr_player_i = 1 - self.curr_player_i
self.player = self.players[self.curr_player_i]
self.player.active = True
def try_move(self, dir, x, y, speed):
if dir == Direction.HORIZ:
next_x = x + speed
if (self.level.map[y, next_x] == '#' or
(self.level.map[y, next_x] == 'R' and not self.level.plates['r'].is_pressed()) or
(next_x == self.players[1-self.curr_player_i].x and y == self.players[1-self.curr_player_i].y)):
return x
return next_x
else:
next_y = y + speed
if (self.level.map[next_y, x] == '#' or
(self.level.map[next_y, x] == 'R' and not self.level.plates['r'].is_pressed()) or
(next_y == self.players[1-self.curr_player_i].y and x == self.players[1-self.curr_player_i].x)):
return y
return next_y
def update(self):
# to facilitate usage
p1 = self.players[0]
p2 = self.players[1]
# Pressure plate check
for plate in self.level.plates.values():
if((p1.rect.x, p1.rect.y) == (plate.x, plate.y) and
p1.powers[p1.current_power_i] == Powers.PRESS.value) or ((p2.rect.x, p2.rect.y) == (plate.x, plate.y) and
p2.powers[p2.current_power_i] == Powers.PRESS.value):
plate.set_pressed(True)
else:
plate.set_pressed(False)
# check for arrival
self.blue_arrive = self.level.map[p1.y, p1.x] == "B"
self.green_arrive = self.level.map[p2.y, p2.x] == "G"
# Check completed condition
if(self.blue_arrive and self.green_arrive):
self.completed = True
def draw(self, screen):
self.level.draw(screen, self.pressing)
def draw_gui(self, screen, time, font):
y = Y_OFFSET
# Blue pane, then blue text box with the player's power
y = self._blit_panel(screen, self.blue_pane, y)
y = self._blit_panel(screen, self.blue_text, y, self.players[0].current_power, BLUE, font)
# Green pane, then green text box with the player's power
y = self._blit_panel(screen, self.green_pane, y)
y = self._blit_panel(screen, self.green_text, y, self.players[1].current_power, GREEN, font)
# Timer box with the elapsed seconds
self._blit_panel(screen, self.timer, y, str(time).split('.')[0], RED, font)
def _blit_panel(self, screen, image, y, text=None, color=None, font=None):
x = (X_OFFSET - image.get_width()) / 2
screen.blit(image, (x, y))
if text is not None:
label = TextArea(text, color)
label.place(x + image.get_width() / 2, y + image.get_height() / 2)
label.draw(screen, None, font, anchor="center")
return y + image.get_height()
# ---------------------------------------------------------------------
# Class MapElement
# ---------------------------------------------------------------------
# This is more of an abstract class that grants common method signature
# and behaviour.
# It inherits from Sprite so that I can add it to the group and
# it already has the draw method implemented
# ---------------------------------------------------------------------
class MapElement(pygame.sprite.Sprite):
def __init__(self, col, row, type):
super().__init__()
self.col = col
self.row = row
self.x = col * TILE_SIZE + X_OFFSET
self.y = row * TILE_SIZE + Y_OFFSET
self.image = self.load_image(type)
self.rect = self.image.get_rect()
self.rect.topleft = (self.x, self.y)
def load_image(self, type):
return pygame.transform.smoothscale(pygame.image.load(TILE_FILES.get(type, TILE_FILES[TileTypes.FLOOR])).convert_alpha(), (TILE_SIZE, TILE_SIZE))
def draw(self, screen):
screen.blit(self.image, self.rect)
# ----------------------------------------------------------------------
# Class PressurePlate
# ----------------------------------------------------------------------
class PressurePlate(MapElement):
def __init__(self, col, row, type):
super().__init__(col, row, type)
self.pressed = False
self.door = None
print("Plate created!")
# I should do the check if the value is boolean, but I do not want to
# (all of my methods do not have such checks, which is a bit ugh)
def set_door(self, door):
self.door = door
def set_pressed(self, value):
self.pressed = value
if self.door is None:
raise ValueError("Door is None")
self.door.set_open(self.pressed)
def is_pressed(self):
return self.pressed
# ----------------------------------------------------------------------
# Class Door
# ----------------------------------------------------------------------
class Door(MapElement):
def __init__(self, col, row, type):
super().__init__(col, row, type)
self.open = False
print("Door created")
def set_open(self, value):
self.open = value
def is_open(self):
return self.open
# I have to ovverride the sprite method, because I have to check if it is opened
def draw(self, screen):
if self.open == False:
super().draw(screen)
else:
pass
# ----------------------------------------------------------------------
# Class GrabbableObject
# ----------------------------------------------------------------------
# represents objects that can be grabbed and put in inventory
# they will be necessary to press certain types of pressure plates
# yet to be implemented
# ----------------------------------------------------------------------
class GrabbableObject(MapElement):
def __init__(self, col, row, type):
super().__init__(col, row, type)
self.grabbed = False
def grab(self):
self.grabbed = True
def release(self):
self.grabbed = False
def is_grabbed(self):
return self.grabbed
# I have to ovverride the sprite method, because I have to check if it is opened
def draw(self, screen):
if self.grabbed == False:
super().draw(screen)
else:
pass