diff --git a/src/ecs.py b/src/ecs.py new file mode 100644 index 0000000..ad61ff8 --- /dev/null +++ b/src/ecs.py @@ -0,0 +1,242 @@ +""" +Un système de gestion d'entitées par les composants. + +Dans ce système, un monde contient des entitiés qui contiennent des composants. +Pour moddifier le monde, on n'agis que sur les composants. +""" + + +from typing import Iterator, Optional, Sequence, TypeVar + + +class Entity: + """ + Une entité dans le monde. + + Cette classe ne contient pas les composants, elle est juste un + utilitaire pour acceder au monde plus facilement. + """ + + __T = TypeVar("__T") + """ + Ce type est utilisé pour permettre la gestion des types + pour certaines fonctions. Par exemple, la fonction `__getitem__` + utilise cette variable pour retourner un object du type demandé. + Cela permet d'avoir des vérifications de types et de l'autocomplétion + en utilisant notre IDE. + """ + + def __init__(self, world: "World", identifier: int) -> None: + self.__world = world + self.__identifier = identifier + + @property + def world(self) -> "World": + """ + Le monde dans lequel se trouve l'entité. + """ + return self.__world + + @property + def identifier(self) -> int: + """ + L'identifiant de l'entité dans le monde. + """ + return self.__identifier + + def __repr__(self) -> str: + return f"Entity({self.__identifier})" + + def __getitem__(self, component_type: type[__T]) -> __T: + return self.__world.get_component(self, component_type) + + def __delitem__(self, component_type: type[object]): + self.__world.remove_component(self, component_type) + + def __setitem__(self, component_type: type[__T], component: __T): + if component_type != type(component): + component = component_type(component) + self.__world.set_component(self, component) + + def __contains__(self, component_type: type[object]) -> bool: + return self.__world.has_component(self, component_type) + + def __len__(self) -> int: + return len(self.__world.all_components(self)) + + def __iter__(self) -> Iterator[object]: + return iter(self.__world.all_components(self)) + + def get(self, component_type: type[__T], default: Optional[__T] = None) -> __T: + """ + Renvoie le composant de type `component_type` de l'entité. + Si aucun composant de type `component_type` n'est dans l'entité: + - Si `default` est None, une exception sera levé. + - Sinon, `default` sera renvoyé. + + Paramètres: + - `component_type`: le type du composant à obtenir. + - `default`: le composant renvoyé si aucun composant de type + `component_type` n'est dans l'entité. + """ + return self.__world.get_component(self, component_type, default) + + def set(self, *components: object): + """ + Ajoute des composants a l'entité. Si un composant du même type est + déja dans l'entité, le composant sera remplacé par le nouveau. + + Paramètres: + - `*components`: les composants à ajouter. + """ + for component in components: + self.__world.set_component(self, component) + + def remove(self, *component_types: type[object]): + """ + Supprime les composants de type `*component_types` de l'entité. Si il n'y a pas de + composant de type `*component_types` dans l'entité, la fonction ne fait rien. + + Paramètres: + - `*component_types`: les types de composant à supprimer. + """ + for component_type in component_types: + self.__world.remove_component(self, component_type) + + def destroy(self): + """ + Supprime tous les composants de l'entité. + """ + self.remove(*[type(component) for component in self]) + + +class World(Entity): + """ + Un monde qui contient les entités. + + Le monde hérite de `Entity` ce qui permet de stoquer des composants + globaux, relatif au monde. + """ + + __T = TypeVar("__T") + """ + Ce type est utilisé pour permettre la gestion des types + pour certaines fonctions. Par exemple, la fonction `get_component` + utilise cette variable pour retourner un object du type demandé. + Cela permet d'avoir des vérifications de types et de l'autocomplétion + en utilisant notre IDE. + """ + + def __init__(self): + super().__init__(self, 0) + self.__components: dict[int, dict[type[object], object]] = {} + self.__entities: dict[type[object], set[int]] = {} + self.__next_id: int = 1 + + def new_entity(self) -> "Entity": + """ + Créer une nouvelle entité dans le monde et la renvoie. + + Techiquelement, cette fonction ne créer pas de nouvelle entité, + car une entité n'est stoqué que si elle a au moins un composant. + Cette fonction renvoie la classe utilitaire `Entity` qui contient + juste un identifiant unique pour cette entité afin de pouvoir + ajouter des composants à cette entité plus tard. + """ + entity = Entity(self, self.__next_id) + self.__next_id += 1 + return entity + + def set_component(self, entity: "Entity", component: object): + """ + Ajoute un composant a une entité. Si un composant du même type est + déja dans l'entité, le composant sera remplacé par le nouveau. + + Paramètres: + - `entity`: l'entité dans lequelle ajouter le composant. + - `component`: le composant à ajouter. + """ + self.__components.setdefault(entity.identifier, {})[type(component)] = component + self.__entities.setdefault(type(component), set()).add(entity.identifier) + + def get_component( + self, entity: "Entity", component_type: type[__T], default: Optional[__T] = None + ) -> __T: + """ + Renvoie le composant de type `component_type` de l'entité `entity`. + Si aucun composant de type `component_type` n'est dans l'entité: + - Si `default` est None, une exception sera levé. + - Sinon, `default` sera renvoyé. + + Paramètres: + - `entity`: l'entité dont on veut obtenir le composant. + - `component_type`: le type du composant à obtenir. + - `default`: le composant renvoyé si aucun composant de type + `component_type` n'est dans l'entité. + """ + component = self.__components.get(entity.identifier, {}).get( + component_type, default + ) + if component is None: + raise ValueError( + f"Entity {entity} has no component of type {component_type}." + ) + return component # type: ignore + + def has_component(self, entity: "Entity", component_type: type[object]) -> bool: + """ + Renvoie si l'entité `entity` a un composant de type `component_type`. + + Paramètres: + - `entity`: l'entité dont on veut savoir si il a un composant. + - `component_type`: le type du composant à verifier. + """ + return component_type in self.__components.get(entity.identifier, {}) + + def remove_component(self, entity: "Entity", component_type: type[object]): + """ + Supprime le composant de type `component_type` de l'entité `entity`. Si il n'y a pas de + composant de type `component_type` dans l'entité, la fonction ne fait rien. + + Paramètres: + - `entity`: l'entité dont on veut supprimer le composant. + - `component_type`: le type du composant à supprimer. + """ + entity_components = self.__components.get(entity.identifier, {}) + component = entity_components.pop(component_type, None) + if component is not None: + self.__entities[component_type].remove(entity.identifier) + if len(entity_components) == 0: + del self.__components[entity.identifier] + + def all_components(self, entity: "Entity") -> list[object]: + """ + Renvoie une liste de tous les composants qui sont dans l'entité `entity`. + + Paramètres: + - `entity`: l'entité dont on veut obtenir les composants. + """ + return list(self.__components.get(entity.identifier, {}).values()) + + def query( + self, *needed: type[object], without: Sequence[type[object]] = () + ) -> set["Entity"]: + """ + Renvoie un set d'`Entity` qui ont tous les composants de type *needed + et qui n'ont aucun des composants de type *without. + + Paramètres: + - `*needed`: les types de composants des entités a renvoyer. + - `without`: les types de composants des entités a exclure. + """ + entities = ( + set(self.__components.keys()) + if len(needed) == 0 + else set(self.__entities.get(needed[0], set())) + ) + entities.difference_update((0,)) + for component_type in needed[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 {Entity(self, entity_id) for entity_id in entities}