Nouveau système d'ECS

This commit is contained in:
Tipragot 2023-10-30 17:30:56 +01:00
parent c4fefba8a9
commit 26bfb318ff
11 changed files with 45 additions and 1819 deletions

View file

@ -9,13 +9,10 @@
"python.analysis.inlayHints.functionReturnTypes": true,
"python.analysis.inlayHints.pytestParameters": true,
"python.analysis.typeCheckingMode": "strict",
"python.analysis.diagnosticSeverityOverrides": {
"reportPrivateUsage": "none"
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"pylint.args": [
"--disable=broad-exception-caught,protected-access,undefined-variable,import-error,unused-import,no-member",
"--disable=protected-access,undefined-variable,import-error,unused-import,no-member",
],
}

View file

@ -1,614 +1,16 @@
"""
Un moteur de jeu inspiré de bevy.
Permet de lancer un jeu et de gérer la boucle principale de celui-ci.
"""
import json
import math
import os
import random
from typing import Callable, Optional, Sequence, SupportsFloat, TypeVar, Union
from time import time
import pygame
from typing import Callable
class World:
"""
Un monde contenant toutes les données du jeu.
"""
__T = TypeVar("__T")
"""
Permet d'indiquer le type d'un composant ou d'une ressource.
"""
def __init__(self):
"""
Constructeur d'un nouveau monde vide.
"""
self._entities: dict[type[object], set[Entity]] = {}
self.__resources: dict[type[object], object] = {}
def create_entity(self, *components: object) -> "Entity":
"""
Crée une nouvelle entité avec les composants donnés et l'ajoute au monde.
Paramètres:
*components: les composants de l'entité.
Retourne:
L'entité.
"""
return Entity(self, *components)
def __setitem__(self, resource_type: type[__T], resource: __T):
if resource_type != type(resource):
raise TypeError()
self.__resources[resource_type] = resource
def __getitem__(self, resource_type: type[__T]) -> __T:
return self.__resources[resource_type] # type: ignore
def __contains__(self, resource_type: type[object]) -> bool:
return resource_type in self.__resources
def __delitem__(self, resource_type: type[object]):
del self.__resources[resource_type]
def query(
self, *component_types: type[object], without: Sequence[type[object]] = ()
) -> set["Entity"]:
"""
Renvoie les entités qui ont tous les composants de type *component_types
et qui n'ont aucun des composants de type *without.
Paramètres:
*component_types: les types de composants des entités a renvoyer.
without: les types de composants des entités a exclure.
Retourne:
Les entités qui correspondent aux critères.
"""
if len(component_types) == 0:
return set()
entities = set(self._entities.get(component_types[0], set()))
for component_type in component_types[1:]:
entities.intersection_update(self._entities.get(component_type, set()))
for component_type in without:
entities.difference_update(self._entities.get(component_type, set()))
return entities
class Entity:
"""
Une entité du monde. Elle contient des composants.
"""
__T = TypeVar("__T")
"""
Permet d'indiquer le type d'un composant.
"""
def __init__(self, world: World, *components: object):
"""
Crée une nouvelle entité avec les composants donnés et l'ajoute au monde.
Paramètres:
world: le monde dans lequel ajouter l'entité
*components: les composants à ajouter à l'entité.
"""
self.__world = world
self.__components: dict[type[object], object] = {}
for component in components:
self[type(component)] = component
def __setitem__(self, component_type: type[__T], component: __T):
if component_type != type(component):
raise TypeError()
self.__components[component_type] = component
self.__world._entities.setdefault(component_type, set()).add(self)
def __getitem__(self, component_type: type[__T]) -> __T:
return self.__components[component_type] # type: ignore
def __contains__(self, component_type: type[object]) -> bool:
return component_type in self.__components
def __delitem__(self, component_type: type[object]):
if self.__components.pop(component_type, None) is not None:
self.__world._entities[component_type].remove(self)
if len(self.__world._entities[component_type]) == 0:
del self.__world._entities[component_type]
def __repr__(self) -> str:
return f"Entity({id(self)})"
class Vec2:
"""
Un vecteur 2D
"""
def __init__(self, *args: Union[SupportsFloat, "Vec2"]):
if (
len(args) == 2
and isinstance(args[0], SupportsFloat)
and isinstance(args[1], SupportsFloat)
):
self.x = float(args[0])
self.y = float(args[1])
elif len(args) == 1:
if isinstance(args[0], Vec2):
self.x = args[0].x
self.y = args[0].y
else:
self.x = float(args[0])
self.y = float(args[0])
elif len(args) == 0:
self.x = 0.0
self.y = 0.0
else:
raise ValueError("Invalid number of arguments")
def __add__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x + other.x, self.y + other.y)
elif isinstance(other, SupportsFloat):
return Vec2(self.x + float(other), self.y + float(other))
raise ValueError(
f"Unsupported operand type(s) for +: 'Vec2' and '{type(other)}'"
)
def __sub__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x - other.x, self.y - other.y)
elif isinstance(other, SupportsFloat):
return Vec2(self.x - float(other), self.y - float(other))
raise ValueError(
f"Unsupported operand type(s) for -: 'Vec2' and '{type(other)}'"
)
def __mul__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x * other.x, self.y * other.y)
elif isinstance(other, SupportsFloat):
return Vec2(self.x * float(other), self.y * float(other))
raise ValueError(
f"Unsupported operand type(s) for *: 'Vec2' and '{type(other)}'"
)
def __truediv__(self, other: object) -> "Vec2":
if isinstance(other, Vec2):
return Vec2(self.x / other.x, self.y / other.y)
elif isinstance(other, SupportsFloat):
return Vec2(self.x / float(other), self.y / float(other))
raise ValueError(
f"Unsupported operand type(s) for /: 'Vec2' and '{type(other)}'"
)
def __eq__(self, other: object) -> bool:
if isinstance(other, Vec2):
return self.x == other.x and self.y == other.y
return False
def __hash__(self) -> int:
return hash((self.x, self.y))
def __neg__(self) -> "Vec2":
return Vec2(-self.x, -self.y)
@property
def length(self) -> float:
"""
Retourne la longueur du vecteur.
"""
return math.sqrt(self.x**2 + self.y**2)
@property
def normalized(self) -> "Vec2":
"""
Retourne une version normalisé du vecteur.
"""
length = self.length
return Vec2(self.x / length, self.y / length)
def __repr__(self) -> str:
return f"Vec2({self.x}, {self.y})"
class Display:
"""
Une classe utilitaire pour la gestion de la fenêtre du jeu.
"""
WIDTH = 1440.0
HEIGHT = 1080.0
RATIO = WIDTH / HEIGHT
INVERT_RATIO = HEIGHT / WIDTH
@staticmethod
def _calculate_surface_rect() -> tuple[float, float, float, float]:
"""
Calcule et renvoit le rectangle de la surface dans la fenêtre du jeu.
"""
width, height = pygame.display.get_surface().get_size()
if width / height < Display.RATIO:
target_height = width * (Display.INVERT_RATIO)
offset = (height - target_height) / 2
rect = (0.0, offset, float(width), target_height)
else:
target_width = height * (Display.RATIO)
offset = (width - target_width) / 2
rect = (offset, 0.0, target_width, float(height))
return rect
class Game:
"""
Un ressource qui représente le jeu actuel.
"""
def __init__(self):
self.stop_requested = False
self.next_scene: Optional[str] = None
def stop(self):
"""
Demande l'arrêt du jeu.
"""
self.stop_requested = True
def change_scene(self, scene_name: str):
"""
Demande un changement de scène.
Paramètres:
scene_name: Le nom de la scène dans laquelle aller.
"""
self.next_scene = scene_name
class Assets:
"""
Resource qui permet la gestion des assets.
"""
def __init__(self, surface: pygame.Surface):
# Création de la texture d'erreur
error_texture = pygame.Surface((256, 256))
error_texture.fill((0, 0, 0))
pygame.draw.rect(error_texture, (255, 0, 255), (0, 0, 128, 128))
pygame.draw.rect(error_texture, (255, 0, 255), (128, 128, 128, 128))
self.__error_texture = error_texture.convert(surface)
# Cache des ressources
self.__textures: dict[str, pygame.Surface] = {}
self.__fonts: dict[int, pygame.font.Font] = {}
self.__sounds: dict[str, pygame.mixer.Sound] = {}
def get_texture(self, name: str) -> pygame.Surface:
"""
Renvoie la texture qui correspond au nom *name*.
Paramètres:
name: Le nom de la texture.
Retourne:
La texture qui correspond au nom *name*.
"""
texture = self.__textures.get(name)
if texture is None:
if os.path.exists(f"assets/textures/{name}"):
texture = pygame.image.load(f"assets/textures/{name}")
if not name.startswith("animations/"):
texture = texture.convert_alpha()
self.__textures[name] = texture
return texture
return self.__error_texture
return texture
def get_texture_size(self, name: str) -> Vec2:
"""
Renvoie la taille de la texture qui correspond au nom *name*.
Paramètres:
name: Le nom de la texture.
Retourne:
La taille de la texture qui correspond au nom *name*.
"""
return Vec2(*self.get_texture(name).get_size())
def get_font(self, size: int) -> pygame.font.Font:
"""
Renvoie la police qui correspond à la taille *size*.
Paramètres:
size: La taille de la police.
Retourne:
La police qui correspond à la taille *size*.
"""
font = self.__fonts.get(size)
if font is None:
font = pygame.font.Font("assets/font.ttf", size)
self.__fonts[size] = font
return font
def get_text_size(self, text: str, size: int) -> Vec2:
"""
Renvoie la taille d'un texte avec une certaine taille de police.
Paramètres:
text: Le texte.
size: La taille de la police.
Retourne:
La taille d'un texte avec une certaine taille de police.
"""
return Vec2(*self.get_font(size).size(text))
def get_sound(self, name: str) -> pygame.mixer.Sound:
"""
Renvoie le son qui correspond au nom *name*.
Paramètres:
name: Le nom du son.
Retourne:
Le son qui correspond au nom *name*.
"""
sound = self.__sounds.get(name)
if sound is None:
sound = pygame.mixer.Sound(f"assets/sounds/{name}")
self.__sounds[name] = sound
return sound
class Time(float):
"""
Ressource qui represente le temps depuis 1900.
"""
class Delta(float):
"""
Ressource qui represente le temps depuis la dernière frame.
"""
class Keyboard:
"""
Ressource qui représente les entrées utilisateurs sur le clavier à la frame actuelle.
"""
def __init__(self):
self.keys: set[str] = set()
self.pressed: set[str] = set()
self.released: set[str] = set()
def is_key_pressed(self, key_name: str) -> bool:
"""
Renvoie True si la touche *key_name* a commencé a être appuyée pendant la frame actuelle.
Paramètres:
key_name: Le nom de la touche à tester.
Retourne:
True si la touche *key_name* a commencé a être appuyée pendant la frame actuelle.
"""
return key_name in self.pressed
def is_key(self, key_name: str) -> bool:
"""
Renvoie True si la touche *key_name* est actuellement appuyée.
Paramètres:
key_name: Le nom de la touche à tester.
Retourne:
True si la touche *key_name* est actuellement appuyée.
"""
return key_name in self.keys
def is_key_released(self, key_name: str) -> bool:
"""
Renvoie True si la touche *key_name* a été relachée pendant la frame actuelle.
Paramètres:
key_name: Le nom de la touche à tester.
Retourne:
True si la touche *key_name* a été relachée pendant la frame actuelle.
"""
return key_name in self.released
class Mouse:
"""
Ressource qui représente l'état de la souris à la frame actuelle.
"""
def __init__(self):
self.buttons: set[int] = set()
self.pressed: set[int] = set()
self.released: set[int] = set()
self.position: Vec2 = Vec2(0.0, 0.0)
self.delta: Vec2 = Vec2(0.0, 0.0)
def is_button_pressed(self, button: int) -> bool:
"""
Renvoie True si le bouton *button* a commencé a être appuyée pendant la frame actuelle.
Paramètres:
button: Le numéro du bouton à tester.
Retourne:
True si le bouton *button* a commencé a être appuyée pendant la frame actuelle.
"""
return button in self.pressed
def is_button(self, button: int) -> bool:
"""
Renvoie True si le bouton *button* est actuellement appuyé.
Paramètres:
button: Le numéro du bouton à tester.
Retourne:
True si le bouton *button* est actuellement appuyé.
"""
return button in self.buttons
def is_button_released(self, button: int) -> bool:
"""
Renvoie True si le bouton *button* a été relaché pendant la frame actuelle.
Paramètres:
button: Le numéro du bouton à tester.
Retourne:
True si le bouton *button* aLongrightarrow relaché pendant la frame actuelle.
"""
return button in self.released
class Position(Vec2):
"""
Composant qui représente la position d'une entité.
"""
class Centered:
"""
Composant permettant de dire que l'affichage de l'entité doit être centré.
"""
class Offset(Vec2):
"""
Composant qui represente un décalage de la position d'une entité.
"""
class Order(int):
"""
Composant qui represente l'ordre d'affichage d'une entité.
"""
class Texture(str):
"""
Composant qui rerpésente la texture d'une entité.
"""
class HoveredTexture(Texture):
"""
Composant qui represente la texture lorsque l'entité est survolée.
"""
class Animation:
"""
Composant qui représente une animation.
"""
def __init__(
self,
name: str,
callback: Callable[[World, Entity], object] = lambda _w, _e: None,
):
self.name = name
self.callback = callback
with open(
f"assets/textures/animations/{name}/info.json",
"r",
encoding="utf-8",
) as f:
info = json.load(f)
self.end_image: str = info.get("end_image", "")
self.offset = Offset(info["offset"]["x"], info["offset"]["y"])
self.frame_count: int = info["frame_count"]
self.fps: int = info["fps"]
self.time = 0.0
class Text(str):
"""
Composant qui represente un texte.
"""
class TextSize(int):
"""
Composant qui represente la taille d'un texte.
"""
class Color(pygame.Color):
"""
Composant qui represente la couleur d'une entité.
"""
class HoverEnter:
"""
Composant qui marque un entité comme commencée à être survolée.
"""
class Hovered:
"""
Composant qui marque un entité comme survolée.
"""
class HoverExit:
"""
Composant qui marque un entité comme arreté d'être survolée.
"""
class Clickable:
"""
Composant qui marque un entité comme pouvant etre cliquer.
"""
def __init__(self, callback: Callable[[World, Entity], object]):
self.callback = callback
class Sound:
"""
Composant qui une entité emettrant un son.
"""
def __init__(
self,
name: str,
volume: float = 0.5,
loop: bool = False,
callback: Callable[[World, Entity], object] = lambda _w, _e: None,
stop_on_remove: bool = False,
) -> None:
self.name = name
if os.path.isdir(f"assets/sounds/{name}"):
list_files = os.listdir(f"assets/sounds/{name}")
random_file = random.choice(list_files)
self.name = f"{name}/{random_file}"
self.volume = volume
self.loop = loop
self.callback = callback
self.stop_on_remove = stop_on_remove
from ecs import World
class Scene:
"""
Une scène dans le jeu.
Une scène du jeu.
"""
def __init__(
@ -629,241 +31,52 @@ class Scene:
)
def start_game(
scenes: dict[str, Scene],
start_scene: str,
*,
title: str = "Game",
) -> None:
class CurrentScene(str):
"""
Lance le moteur de jeu.
Resource qui permet de savoir et de changer la scène actuelle.
"""
# Initialisation de pygame
pygame.init()
if os.path.exists("icon.png"):
pygame.display.set_icon(pygame.image.load("icon.png"))
pygame.display.set_caption(title)
pygame.display.set_mode((800, 600), pygame.RESIZABLE)
surface = pygame.Surface((Display.WIDTH, Display.HEIGHT))
keyboard = Keyboard()
mouse = Mouse()
# Chargements des assets
assets = Assets(surface)
# creation des channels pour les sons
channels: dict[Entity, tuple[bool, pygame.mixer.Channel]] = {}
class KeepAlive:
"""
Composant qui marque une entité comme n'étant pas détruit lors
d'un changement de scène.
"""
# On récupère la première scène
scene = scenes.get(start_scene)
# Tant qu'il y a des scènes a executer on les executent
def start_game(scenes: dict[str, Scene], scene_name: str):
"""
Lance un jeu.
Paramètres:
- `scenes`: un dictionnaire contenant les scènes du jeu.
- `scene_name`: le nom de la scène à charger en premier lors du lancement du jeu.
"""
world = World()
world[CurrentScene] = CurrentScene(scene_name)
scene = scenes.get(scene_name)
while scene is not None:
# Initialisation du monde
world = World()
world[Game] = Game()
world[Assets] = assets
world[Keyboard] = keyboard
world[Mouse] = mouse
world[Time] = Time(time())
# On retire les ressources de l'ancienne scène
for resource in world:
if not isinstance(resource, KeepAlive):
world.remove(type(resource))
# Initialisation de la scène
# On retire les entité de l'ancienne scène
for entity in world.query(without=(KeepAlive,)):
entity.destroy()
# On initialise la nouvelle scene
for system in scene.init_systems:
system(world)
# Tant que la scène n'est pas terminé
while world[Game].next_scene is None and not world[Game].stop_requested:
# On gère les évenements pygame
keyboard.pressed.clear()
keyboard.released.clear()
mouse.pressed.clear()
mouse.released.clear()
last_position = Vec2(mouse.position)
for event in pygame.event.get():
if event.type == pygame.QUIT:
world[Game].stop()
elif event.type == pygame.KEYDOWN:
key_name = pygame.key.name(event.key)
if key_name == "f11":
pygame.display.toggle_fullscreen()
keyboard.keys.add(key_name)
keyboard.pressed.add(key_name)
elif event.type == pygame.KEYUP:
key_name = pygame.key.name(event.key)
if key_name == "f11":
continue
keyboard.keys.remove(key_name)
keyboard.released.add(key_name)
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse.buttons.add(event.button)
mouse.pressed.add(event.button)
elif event.type == pygame.MOUSEBUTTONUP:
mouse.buttons.remove(event.button)
mouse.released.add(event.button)
elif event.type == pygame.MOUSEMOTION:
rect = Display._calculate_surface_rect()
mouse.position = Vec2(
((event.pos[0] - rect[0]) / rect[2]) * Display.WIDTH,
((event.pos[1] - rect[1]) / rect[3]) * Display.HEIGHT,
)
mouse.delta = mouse.position - last_position
# On vérifie le survol des textures et textes
for entity in world.query(Position, Texture):
# Récupération de la position et taille de l'entité
position: Vec2 = entity[Position]
if Offset in entity:
position = position + entity[Offset]
size = assets.get_texture_size(entity[Texture])
if Centered in entity:
position -= size / 2
# On détermine si la souris est sur l'entité
if (
mouse.position.x >= position.x
and mouse.position.x <= position.x + size.x
and mouse.position.y >= position.y
and mouse.position.y <= position.y + size.y
):
if Hovered not in entity:
entity[HoverEnter] = HoverEnter()
else:
del entity[HoverEnter]
entity[Hovered] = Hovered()
else:
if Hovered in entity:
entity[HoverExit] = HoverExit()
else:
del entity[HoverExit]
del entity[Hovered]
# On met à jour le temps et delta
now = time()
world[Delta] = Delta(now - world[Time])
world[Time] = Time(now)
# On execute les objets clickables
if mouse.is_button_pressed(1):
for entity in world.query(Clickable, Hovered):
entity[Clickable].callback(world, entity)
# On met à jour la scène
# Tant que la scène n'est pas terminé on la met à jour
while world.get(CurrentScene, "") == scene_name:
for system in scene.update_systems:
system(world)
# Gestion des sons en cours
sound_entities = world.query(Sound)
entities_to_delete: list[Entity] = []
for entity, (stop_on_remove, channel) in channels.items():
if Sound in entity:
entity_sound = entity[Sound]
channel.set_volume(entity_sound.volume)
if not channel.get_busy():
entities_to_delete.append(entity)
del entity[Sound]
entity_sound.callback(world, entity)
continue
if stop_on_remove and entity not in sound_entities:
entities_to_delete.append(entity)
channel.stop()
for entity in entities_to_delete:
del channels[entity]
# Ajout des sons non gérés
for entity in world.query(Sound):
if entity not in channels:
entity_sound = entity[Sound]
sound = assets.get_sound(entity_sound.name)
channel = sound.play(loops=-1 if entity_sound.loop else 0)
if channel is None: # type: ignore
continue
channel.set_volume(entity_sound.volume)
channels[entity] = entity_sound.stop_on_remove, channel
# Mise à jour des animations
for entity in world.query(Animation):
animation = entity[Animation]
if animation.time == 0:
if animation.end_image == "" and Texture in entity:
animation.end_image = entity[Texture]
animation.time += world[Delta]
frame_index = int(animation.time * animation.fps)
if frame_index >= animation.frame_count:
entity[Texture] = Texture(animation.end_image)
del entity[Animation]
del entity[Offset]
animation.callback(world, entity)
else:
entity[Offset] = Offset(animation.offset.x, animation.offset.y)
entity[Texture] = Texture(
f"animations/{animation.name}/{frame_index:04}.png"
)
# Rendu du monde
for entity in sorted(world.query(Order, Position), key=lambda e: e[Order]):
# Récupération de la position de l'entité
position: Vec2 = entity[Position]
if Offset in entity:
position = position + entity[Offset]
centered = Centered in entity
# Affichage de la texture
if Texture in entity:
if HoveredTexture in entity and Hovered in entity:
texture = assets.get_texture(entity[HoveredTexture])
else:
texture = assets.get_texture(entity[Texture])
surface.blit(
texture,
(
position.x - texture.get_width() / 2
if centered
else position.x,
position.y - texture.get_height() / 2
if centered
else position.y,
),
)
# Affichage des textes
if Text in entity:
color = entity[Color] if Color in entity else Color(255, 255, 255)
size = entity[TextSize] if TextSize in entity else 50
font = assets.get_font(size)
font_surface = font.render(entity[Text], True, color)
surface.blit(
font_surface,
(
position.x - font_surface.get_width() / 2
if centered
else position.x,
position.y - font_surface.get_height() / 2
if centered
else position.y,
),
)
# Mise a jour de la fenêtre
rect = Display._calculate_surface_rect()
pygame.transform.set_smoothscale_backend("MMX")
pygame.transform.smoothscale(
surface,
(rect[2], rect[3]),
pygame.display.get_surface().subsurface(rect),
)
pygame.display.flip()
surface.fill((0, 0, 0))
# Arrêt de la scène
# On arrete la scene
for system in scene.stop_systems:
system(world)
# Récupération de la scène suivante
next_scene = world[Game].next_scene
if next_scene is not None:
scene = scenes.get(next_scene)
else:
scene = None
# Arrêt du jeu
pygame.quit()
scene = scenes.get(world.get(CurrentScene, ""))

View file

@ -1,19 +1,3 @@
"""
Example de l'utilisation du moteur de jeu.
Module d'exemple de l'utilisation du moteur de jeu.
"""
from engine import start_game
from scenes import classique, menteur, menu, directory_search
start_game(
{
"menu": menu.SCENE,
"classique": classique.SCENE,
"menteur": menteur.SCENE,
"histoire": directory_search.SCENE,
},
"menu",
title="Guess The Number",
)

View file

@ -1,3 +1,10 @@
"""
Contient des plugins pour certaines features du jeu.
Module contenant tous les plugins du jeu.
Un plugin est une scène pouvant être ajouté a d'autres scènes
afin d'ajouter des fonctionnalités au jeu.
Le but est de faire en sorte que les plugins soient génériques
afin de pouvoir les utilisers dans plusieurs scènes et donc
éviter de répéter plusieurs fois le même code.
"""

View file

@ -1,37 +0,0 @@
"""
Un plugin permettant de faire des déplacements fluides des entités.
"""
from engine import Delta, Position, Scene, Vec2, World
class Target(Vec2):
"""
Composant donnant la position voulue de l'entité.
"""
class Speed(float):
"""
Composant donnant la vittesse de déplacement de l'entité.
"""
def __update_positions(world: World):
"""
Met à jour la position des entités pour se rapprocher de la position voulue.
"""
for entity in world.query(Position, Target):
position = entity[Position]
target = entity[Target]
speed = entity[Speed] if Speed in entity else Speed(10)
entity[Position] = Position(
position + (target - position) * world[Delta] * speed
)
PLUGIN = Scene(
[],
[__update_positions],
[],
)

View file

@ -1,37 +0,0 @@
"""
Definit un plugin qui crée un texte avec les touches frappées
"""
from engine import Keyboard, Scene, Sound, Text, World
class Typing(str):
"""
Marque une entité comme un texte qui s'ecrit en fonction du clavier
"""
def __update(world: World):
"""
Met a jour les entitées contenant le composant Typing
"""
keyboard = world[Keyboard]
for entity in world.query(Typing, Text):
text = entity[Text]
for key in keyboard.pressed:
if key == "backspace":
world.create_entity(Sound("click"))
text = text[:-1]
if key.startswith("["): # pavé numerique
key = key[1]
if key in entity[Typing]:
world.create_entity(Sound("click"))
text += key
entity[Text] = Text(text)
PLUGIN = Scene(
[],
[__update],
[],
)

View file

@ -1,3 +0,0 @@
"""
Contient toutes les scènes du jeu.
"""

View file

@ -1,227 +0,0 @@
"""
Définis la scène du jeu classique, sans variante.
"""
import random
from plugins import typing
from engine import (
Centered,
Clickable,
Color,
Display,
Entity,
Game,
HoveredTexture,
Keyboard,
Order,
Position,
Scene,
Sound,
Text,
TextSize,
Texture,
World,
)
COLOR_TEXT = Color(66, 39, 148)
class RandomNumber(int):
"""
La ressource qui est le nombre a deviner.
"""
class TextDialogue:
"""
Le component qui declare l'entitee Text qui affiche le plus petit ou le plus grand
"""
class NombreEssai(int):
"""
Le component qui declare le nombre d'essai
"""
class NombreEssaiText:
"""
Le component qui affiche le nombre d'essai
"""
class IsRunning:
"""
Le component qui indique si le jeu est en cours
"""
def __initialize_world(world: World):
"""
Initialise le monde du menu.
"""
# Fond d'ecran
world.create_entity(
Position(),
Order(0),
Texture("classique/background.png"),
)
# Bouton valider/rejouer
world.create_entity(
Position(Display.WIDTH / 2, 875),
Order(1),
Centered(),
Texture("classique/valider.png"),
HoveredTexture("classique/valider_hover.png"),
Clickable(lambda world, _: _update(world)),
)
# Zone de saisie
world.create_entity(
Position(Display.WIDTH / 2, 750),
Order(2),
Centered(),
typing.Typing("1234567890"),
Text(""),
COLOR_TEXT,
TextSize(150),
)
# Text qui dit si ton nombre et trop grand ou trop petit
world.create_entity(
Position(Display.WIDTH / 2, 500),
Order(3),
Centered(),
TextDialogue(),
TextSize(150),
COLOR_TEXT,
Text("Devine le nombre..."),
)
# Text qui affiche le nombre d'essai
world.create_entity(
Position(Display.WIDTH / 2 - 100, 150),
Order(4),
TextSize(100),
NombreEssaiText(),
COLOR_TEXT,
Text("il reste : 7 essais"),
)
# Bouton pour revenir au menu
world.create_entity(
Order(11),
Position(150, 150),
Texture("classique/arrow.png"),
Clickable(on_menu_button),
HoveredTexture("classique/arrow_hover.png"),
)
# Les ressources.
world[NombreEssai] = NombreEssai(7)
world[RandomNumber] = RandomNumber(random.randint(0, 99))
world[IsRunning] = IsRunning()
def on_menu_button(world: World, entity: Entity):
"""
Fonction qui s'execute quand on clique sur un bouton.
"""
world[Game].change_scene("menu")
entity[Sound] = Sound("click")
def _update(world: World):
"""
Verifie si le nombre donné est le meme que celui que l'on a choisi.
Boucle du jeu.
"""
world.create_entity(Sound("menu_click.wav"))
# si le jeu s'est arrete.
if IsRunning not in world:
# on relance le jeu.
world[Game].change_scene("classique")
for entity in world.query(typing.Typing, Text):
# One efface le nombre.
number: str = entity[Text]
entity[Text] = Text("")
# On gere le l'input de l'utilisateur.
for entity_text in world.query(TextDialogue):
if number == "": # si il a rien evoyé.
entity_text[Text] = Text("tu doit entrer un nombre !")
return
if world[RandomNumber] == int(number): # si il a trouve le nombre.
end_game(world, "Gagné")
return
elif world[NombreEssai] <= 1: # si il n'a plus d'essai.
end_game(world, "Perdu")
return
elif world[RandomNumber] > int(number): # si le nombre est trop petit.
entity_text[Text] = Text("Plus grand...")
else: # si le nombre est trop grand.
entity_text[Text] = Text("Plus petit...")
# on update l'affichage du nombre d'essai.
world[NombreEssai] = NombreEssai(world[NombreEssai] - 1)
for entity in world.query(NombreEssaiText):
entity[Text] = Text(
f"il reste : {world[NombreEssai]} essai{'s' if world[NombreEssai] != 1 else ''}"
)
def end_game(world: World, state: str):
"""
fonction applé quand le jeu est fini.
"""
del world[IsRunning] # le jeu est fini.
# On joue le son
if state == "Gagné":
world.create_entity(Sound("win_sound.wav"))
else:
world.create_entity(Sound("lose_sound.wav"))
# On affiche le message de fin.
for entity_text in world.query(TextDialogue):
entity_text[Text] = Text(f"{state} !")
# On empeche de pourvoir continuer le jeu.
for entity in world.query(typing.Typing, Text):
del entity[typing.Typing]
if state == "Gagné":
for entity in world.query(NombreEssaiText):
entity[Text] = Text("")
else:
for entity in world.query(NombreEssaiText):
entity[Text] = Text(" plus d'essais")
# on change la texture du button submit.
for entity in world.query(Clickable, Centered):
entity[Texture] = Texture("classique/play_again.png")
entity[HoveredTexture] = HoveredTexture("classique/play_again_hover.png")
def _check_return(world: World):
"""
Verifie si la touche entrée est appuyée.
"""
keyboard = world[Keyboard]
if keyboard.is_key_pressed("return") or keyboard.is_key_pressed("enter"):
_update(world)
SCENE = (
Scene(
[__initialize_world],
[_check_return],
[],
)
+ typing.PLUGIN
)

View file

@ -1,378 +0,0 @@
"""
Scène du jeu dans lequel on se cache de Edmond dans les dossiers.
"""
from enum import Enum
import random
from engine import (
Animation,
Centered,
Delta,
Display,
Entity,
Hovered,
Mouse,
Order,
Position,
Scene,
Sound,
Text,
TextSize,
Texture,
Vec2,
World,
)
from plugins import smooth
LINES = 3
COLUMNS = 5
SPACING = 200
class State(Enum):
"""
Etat de la scène.
"""
MOVING = 0
SEARCHING = 1
GAME_OVER = 2
class SelectedDirectory:
"""
Une ressource qui stoque le dossier selectionné pour le déplacement.
"""
def __init__(self, entity: Entity, start_position: Vec2):
self.entity = entity
self.position = start_position
class AttackTimer(float):
"""
Ressource qui stoque un timer pour l'attaque.
"""
class AttackSpeed(float):
"""
Ressource qui dit le temps de l'attaque.
"""
class DirectoryPosition:
"""
La position d'un dossier dans la grille.
"""
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __eq__(self, value: object) -> bool:
if isinstance(value, DirectoryPosition):
return self.x == value.x and self.y == value.y
return False
def screen_position(self) -> Vec2:
"""
Calcule la position de l'entité sur l'ecran.
"""
size = Vec2(SPACING)
offset = -(size * Vec2(COLUMNS - 1, LINES - 1) / 2)
first_position = Vec2(Display.WIDTH / 2, Display.HEIGHT / 2) + offset
return first_position + Vec2(self.x, self.y) * size
class AttackPoint(DirectoryPosition):
"""
Composant qui marque un point d'attaque.
"""
class DirectoryName:
"""
Composant qui marque une entité comme étant le nom d'un dossier.
"""
def __init__(self, entity: Entity):
self.entity = entity
class UserDirectory:
"""
Composant qui marque le dossier que l'utilisateur doit protéger.
"""
class GameStarted:
"""
Une ressource qui permet de savoir que le jeu commence.
"""
def __change_folders_speeds(world: World, _e: Entity):
"""
Change les vitesses des dossiers.
"""
for entity in world.query(DirectoryPosition, smooth.Speed):
entity[smooth.Speed] = smooth.Speed(random.uniform(2.0, 2.5))
for entity in world.query(TextSize):
entity[TextSize] = TextSize(40)
def __remove_folders_speeds(world: World):
"""
Supprime les vitesses des dossiers.
"""
if GameStarted not in world and world[AttackTimer] >= 3.0:
for entity in world.query(DirectoryPosition, smooth.Speed):
del entity[smooth.Speed]
world[GameStarted] = GameStarted()
def __initialize_world(world: World):
"""
Initialise le monde de la scène.
"""
world[State] = State.MOVING
world[AttackTimer] = AttackTimer(0.0)
world[AttackSpeed] = AttackSpeed(5.0)
world.create_entity(
Position(),
Order(0),
Animation("fade_desktop", __change_folders_speeds),
)
names = [
"Classique",
"Menteur",
"Tricheur",
"Histoire",
"Je t'aime",
"Hello",
"Cheval",
"Defender",
"Dansons",
"Secrets",
"Edmond",
"Mon Amour",
"Melatonin",
"Films",
"Cinéma",
]
positions = [
Position(40 + (7 * 180) + 48, 35 + (5 * 166) + 38),
Position(40 + (5 * 180) + 48, 35 + (5 * 166) + 38),
Position(40 + (4 * 180) + 48, 35 + (4 * 166) + 38),
Position(40 + (3 * 180) + 48, 35 + (5 * 166) + 38),
Position(40 + (1 * 180) + 48, 35 + (5 * 166) + 38),
Position(40 + (6 * 180) + 48, 35 + (2 * 166) + 38),
Position(40 + (5 * 180) + 48, 35 + (3 * 166) + 38),
Position(40 + (4 * 180) + 48, 35 + (2 * 166) + 38),
Position(40 + (2 * 180) + 48, 35 + (4 * 166) + 38),
Position(40 + (1 * 180) + 48, 35 + (2 * 166) + 38),
Position(40 + (7 * 180) + 48, 35 + (1 * 166) + 38),
Position(40 + (5 * 180) + 48, 35 + (1 * 166) + 38),
Position(40 + (3 * 180) + 48, 35 + (0 * 166) + 38),
Position(40 + (2 * 180) + 48, 35 + (1 * 166) + 38),
Position(40 + (0 * 180) + 48, 35 + (1 * 166) + 38),
]
for y in range(LINES):
for x in range(COLUMNS):
position = DirectoryPosition(x, y)
entity = world.create_entity(
positions.pop(),
smooth.Speed(0),
Order(1),
Centered(),
Texture("directory.png"),
position,
)
if x == 2 and y == 1:
entity[UserDirectory] = UserDirectory()
entity[Texture] = Texture("user_directory.png")
world.create_entity(
Position(0, 0),
Order(1),
Centered(),
Text(names.pop()),
TextSize(0),
DirectoryName(entity),
)
def __attacks(world: World):
"""
Déclenche les attaques de Edmond.
"""
if world[State] == State.GAME_OVER:
return
world[AttackTimer] = AttackTimer(world[AttackTimer] + world[Delta])
timer = world[AttackTimer]
if timer >= world[AttackSpeed] and world[State] == State.MOVING:
world[State] = State.SEARCHING
for entity in world.query(AttackPoint):
position = entity[AttackPoint]
for directory_entity in world.query(DirectoryPosition):
if directory_entity[DirectoryPosition] == position:
if UserDirectory in directory_entity:
directory_entity[Animation] = Animation(
"search_directory_failed"
)
world[State] = State.GAME_OVER
else:
directory_entity[Animation] = Animation("search_directory")
del entity[AttackPoint]
del entity[Position]
del entity[Order]
del entity[Centered]
del entity[Texture]
elif timer >= world[AttackSpeed] + 4.5 and world[State] == State.SEARCHING:
world[State] = State.MOVING
for _ in range(10):
position = AttackPoint(
random.randint(0, COLUMNS - 1),
random.randint(0, LINES - 1),
)
world.create_entity(
position,
Position(position.screen_position()),
Order(50),
Centered(),
Texture("attack_point.png"),
)
world[AttackTimer] = AttackTimer(0.0)
world[AttackSpeed] = AttackSpeed(world[AttackSpeed] * 0.9)
def __move_directories(world: World):
"""
Permet de déplacer les dossiers avec la souris.
"""
# Si on n'est pas dans le bon state on annule
if GameStarted not in world or world[State] == State.GAME_OVER:
return
# On met à jour la séléction
mouse = world[Mouse]
for entity in world.query(Hovered, DirectoryPosition):
if Animation in entity:
continue
if mouse.is_button_pressed(1):
world[SelectedDirectory] = SelectedDirectory(entity, Vec2(mouse.position))
break
# Si un dossier est séléctionné
if SelectedDirectory in world:
selected_directory = world[SelectedDirectory]
selected_entity = selected_directory.entity
directory_position = selected_entity[DirectoryPosition]
# Vérification du relachement de la souris
if not mouse.is_button(1):
del world[SelectedDirectory]
return
# On calcule le déplacement de la souris
mouse_delta = mouse.position - selected_directory.position
# On annule si il y a pas eu de déplacement significatif de la souris
if mouse_delta.length < 40:
return
# Récupération du mouvement voulu
if abs(mouse_delta.x) >= abs(mouse_delta.y):
movement = (int(mouse_delta.x / abs(mouse_delta.x)), 0)
else:
movement = (0, int(mouse_delta.y / abs(mouse_delta.y)))
# Récupération des mouvements possible du dossier
movements: list[tuple[int, int]] = []
if directory_position.x != 0:
movements.append((-1, 0))
if directory_position.x != COLUMNS - 1:
movements.append((1, 0))
if directory_position.y != 0:
movements.append((0, -1))
if directory_position.y != LINES - 1:
movements.append((0, 1))
if len(movements) == 0:
return
# Si le mouvement n'est pas possible, on annule
if movement not in movements:
return
# Si l'entité est animé on annule
if Animation in selected_entity:
return
# On trouve l'autre dossier
for entity in world.query(DirectoryPosition, without=(Animation,)):
if entity != selected_entity and entity[
DirectoryPosition
] == DirectoryPosition(
directory_position.x + movement[0],
directory_position.y + movement[1],
):
other_directory = entity
break
else:
return
# On actualise la position de l'autre dossier
other_directory[DirectoryPosition].x -= movement[0]
other_directory[DirectoryPosition].y -= movement[1]
# On actualise la position du dossier
selected_entity[DirectoryPosition].x += movement[0]
selected_entity[DirectoryPosition].y += movement[1]
# On joue un son
world.create_entity(Sound("slide.wav"))
# On retire le dossier selectionné
del world[SelectedDirectory]
def __update_positions(world: World):
"""
Met à jour la position cible des dossiers.
"""
for entity in world.query(DirectoryPosition):
position = entity[DirectoryPosition]
entity[smooth.Target] = smooth.Target(position.screen_position())
entity[Order] = Order(position.y + 1)
def __update_directory_names(world: World):
"""
Met à jour la position des noms des dossiers.
"""
for entity in world.query(DirectoryName):
directory_entity = entity[DirectoryName].entity
entity[Position] = Position(directory_entity[Position] + Vec2(0, 75))
entity[Order] = directory_entity[Order]
SCENE = (
Scene(
[__initialize_world],
[__attacks, __move_directories, __update_positions, __remove_folders_speeds],
[],
)
+ smooth.PLUGIN
+ Scene(
[],
[__update_directory_names],
[],
)
)

View file

@ -1,236 +0,0 @@
"""
Définis la scène du jeu menteur, sans variante.
"""
from random import randint
from plugins import typing
from engine import (
Centered,
Clickable,
Color,
Display,
Entity,
Game,
HoveredTexture,
Keyboard,
Order,
Position,
Scene,
Sound,
Text,
TextSize,
Texture,
World,
)
class RandomNumber(int):
"""
La ressource qui est le nombre a deviner.
"""
class TextDialogue:
"""
Le component qui declare l'entitee Text qui affiche le plus petit ou le plus grand
"""
class NombreEssai(int):
"""
Le component qui declare le nombre d'essai
"""
class NombreEssaiText:
"""
Le component qui affiche le nombre d'essai
"""
class IsRunning:
"""
Le component qui indique si le jeu est en cours
"""
COLOR_TEXT = Color(59, 162, 0)
def __initialize_world(world: World):
"""
Initialise le monde du menu.
"""
# Fond d'ecran
world.create_entity(
Position(),
Order(0),
Texture("menteur/background.png"),
)
# Bouton valider/rejouer
world.create_entity(
Position(Display.WIDTH / 2, 880),
Order(1),
Centered(),
Texture("menteur/valider.png"),
HoveredTexture("menteur/valider_hover.png"),
Clickable(lambda world, _: _update(world)),
)
# Zone de saisie
world.create_entity(
Position(Display.WIDTH / 2, 750),
Order(2),
Centered(),
typing.Typing("1234567890"),
Text(""),
COLOR_TEXT,
TextSize(150),
)
# Text qui dit si ton nombre et trop grand ou trop petit
world.create_entity(
Position(Display.WIDTH / 2, 500),
Order(3),
Centered(),
TextDialogue(),
TextSize(150),
COLOR_TEXT,
Text("Devine le nombre..."),
)
# Text qui affiche le nombre d'essai
world.create_entity(
Position(Display.WIDTH - 750, 120),
Order(4),
TextSize(100),
NombreEssaiText(),
COLOR_TEXT,
Text("il reste : 15 essais"),
)
# Bouton pour revenir au menu
world.create_entity(
Order(11),
Position(100, 100),
Texture("menteur/arrow.png"),
Clickable(on_menu_button),
HoveredTexture("menteur/arrow_hover.png"),
)
# Les ressources.
world[NombreEssai] = NombreEssai(15)
world[RandomNumber] = RandomNumber(randint(0, 99))
world[IsRunning] = IsRunning()
def on_menu_button(world: World, entity: Entity):
"""
Fonction qui s'execute quand on clique sur un bouton.
"""
world[Game].change_scene("menu")
entity[Sound] = Sound("click")
def _update(world: World):
"""
Verifie si le nombre donné est le meme que celui que l'on a choisi.
Boucle du jeu.
"""
world.create_entity(Sound("menu_click.wav"))
# si le jeu s'est arrete.
if IsRunning not in world:
# on relance le jeu.
world[Game].change_scene("menteur")
for entity in world.query(typing.Typing, Text):
# One efface le nombre.
number: str = entity[Text]
entity[Text] = Text("")
# On gere le l'input de l'utilisateur.
for entity_text in world.query(TextDialogue):
if number == "": # si il a rien evoyé.
entity_text[Text] = Text("tu doit entrer un nombre !")
return
if world[RandomNumber] == int(number): # si il a trouve le nombre.
end_game(world, "Gagné")
return
elif world[NombreEssai] <= 1: # si il n'a plus d'essai.
end_game(world, "Perdu")
return
elif world[RandomNumber] > int(number): # si le nombre est trop petit.
lie = randint(1, 4)
if lie == 4:
entity_text[Text] = Text("Plus petit...")
else:
entity_text[Text] = Text("Plus grand...")
else: # si le nombre est trop grand.
lie = randint(1, 4)
if lie == 4:
entity_text[Text] = Text("Plus grand...")
else:
entity_text[Text] = Text("Plus petit...")
# on update l'affichage du nombre d'essai.
world[NombreEssai] = NombreEssai(world[NombreEssai] - 1)
for entity in world.query(NombreEssaiText):
entity[Text] = Text(
f"il reste : {world[NombreEssai]} essai{'s' if world[NombreEssai] != 1 else ''}"
)
def end_game(world: World, state: str):
"""
fonction applé quand le jeu est fini.
"""
del world[IsRunning] # le jeu est fini.
# On joue le son
if state == "Gagné":
world.create_entity(Sound("win_sound.wav"))
else:
world.create_entity(Sound("lose_sound.wav"))
# On affiche le message de fin.
for entity_text in world.query(TextDialogue):
entity_text[Text] = Text(f"{state} !")
# On empeche de pourvoir continuer le jeu.
for entity in world.query(typing.Typing, Text):
del entity[typing.Typing]
if state == "Gagné":
for entity in world.query(NombreEssaiText):
entity[Text] = Text("")
else:
for entity in world.query(NombreEssaiText):
entity[Text] = Text(" plus d'essais")
# on change la texture du button submit.
for entity in world.query(Clickable, Centered):
entity[Texture] = Texture("menteur/play_again.png")
entity[HoveredTexture] = HoveredTexture("menteur/play_again_hover.png")
def _check_return(world: World):
"""
Verifie si la touche entrée est appuyée.
"""
keyboard = world[Keyboard]
if keyboard.is_key_pressed("return") or keyboard.is_key_pressed("enter"):
_update(world)
SCENE = (
Scene(
[__initialize_world],
[_check_return],
[],
)
+ typing.PLUGIN
)

View file

@ -1,57 +0,0 @@
"""
Définis la scène du menu du jeu.
"""
from engine import (
Centered,
Clickable,
Display,
Entity,
Game,
HoveredTexture,
Order,
Position,
Scene,
Sound,
Texture,
World,
)
def __create_button(world: World, i: int, name: str):
"""
Ajoute un bouton au monde.
"""
world.create_entity(
Position(Display.WIDTH / 2, 450 + 150 * i),
Order(1),
Centered(),
Texture(f"menu/button_{name}.png"),
HoveredTexture(f"menu/button_{name}_hover.png"),
Clickable(lambda world, entity: on_click_butons(world, entity, name)),
)
def on_click_butons(world: World, entity: Entity, name: str):
"""
Fonction qui s'execute quand on clique sur un bouton.
"""
entity[Sound] = Sound("click")
world[Game].change_scene(name)
def __initialize_world(world: World):
"""
Initialise le monde du menu.
"""
world.create_entity(Position(), Order(0), Texture("menu/background.png"))
scenes_name = ["classique", "menteur", "tricheur", "histoire"]
for i, name in enumerate(scenes_name):
__create_button(world, i, name)
SCENE = Scene(
[__initialize_world],
[],
[],
)