diff --git a/.gitignore b/.gitignore index 7221e66..99e64f0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ __pycache__ # Don't commit settings settings.json + +# Don't commit game save +save.json diff --git a/README.md b/README.md index 3e3e740..63d7374 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Dungeon Battle -M1 Software engineering project +Projet de génie logiciel de M1 ## Création d'un environnement de développement @@ -33,3 +33,70 @@ Il est toujours préférable de travailler dans un environnement Python isolé d (env)$ pip3 install -r requirements.txt (env)$ deactivate # sortir de l'environnement ``` + +### Exécution des tests + +Les tests sont gérés par `pytest` dans le module `dungeonbattle.tests`. + +`tox` est un outil permettant de configurer l'exécution des tests. Ainsi, après +installation de tox dans votre environnement virtuel via `pip install tox`, +il vous suffit d'exécuter `tox -e py3` pour lancer les tests et `tox -e linters` +pour vérifier la syntaxe du code. + + +## Lancement du jeu + +Il suffit d'exécuter `python3 main.py`. + +## Gestion des émojis + +Le jeu dispose de deux modes graphiques : en mode `ascii` et `squirrel`. +Le mode `squirrel` affiche des émojis pour un meilleur affichage. Toutefois, +il est possible que vous n'ayez pas les bonnes polices. + +### Sous Windows + +Sous Windows, vous devriez avoir les bonnes polices installées nativement. + +### Sous Arch Linux + +Il est recommandé d'utiliser le terminal `xfce4-terminal`. Il suffit d'installer +le paquets de polices + +```bash +sudo pacman -Sy noto-fonts-emoji +``` + +Le jeu doit ensuite se lancer normalement sans action supplémentaire. + +### Sous Ubuntu/Debian + +À nouveau, le terminal `xfce4-terminal` est recommandé. Le paquet +`fonts-noto-color-emoji`. Toutefois, le rythme de mise à jour de Debian étant +lent, le paquet le plus récent ne contient pas tous les émojis. Sur Debian, +il faudra donc installer le paquet le plus récent, ce qui fonctionne sans +dépendance supplémentaire : + +```bash +wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb +dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb +rm fonts-noto-color-emoji_0~20200916-1_all.deb +``` + +Il reste le problème de l'écureuil. Sous Ubuntu et Debian, le caractère écureuil +existe déjà, mais ne s'affiche pas proprement. On peut appliquer un patch qui +permet d'afficher les émojis correctement dans son terminal. Pour cela, il + suffit de faire : + +```bash +ln -s $PWD/fix-squirrel-emojis.conf /etc/fonts/conf.avail/75-fix-squirrel-emojis.conf +ln -s /etc/fonts/conf.avail/75-fix-squirrel-emojis.conf /etc/fonts/conf.d/75-fix-squirrel-emojis.conf +``` + +Après redémarrage du terminal, l'écureuil devrait s'afficher correctement. + +Pour supprimer le patch : + +```bash +rm /etc/fonts/conf.d/75-fix-squirrel-emojis.conf +``` diff --git a/dungeonbattle/__init__.pyc b/dungeonbattle/__init__.pyc new file mode 100644 index 0000000..c512440 Binary files /dev/null and b/dungeonbattle/__init__.pyc differ diff --git a/dungeonbattle/bootstrap.py b/dungeonbattle/bootstrap.py index 0bc97be..9828fae 100644 --- a/dungeonbattle/bootstrap.py +++ b/dungeonbattle/bootstrap.py @@ -4,6 +4,12 @@ from dungeonbattle.term_manager import TermManager class Bootstrap: + """ + The bootstrap object is used to bootstrap the game so that it starts + properly. + (It was initially created to avoid circular imports between the Game and + Display classes) + """ @staticmethod def run_game(): diff --git a/dungeonbattle/bootstrap.pyc b/dungeonbattle/bootstrap.pyc new file mode 100644 index 0000000..80c152b Binary files /dev/null and b/dungeonbattle/bootstrap.pyc differ diff --git a/dungeonbattle/entities/items.py b/dungeonbattle/entities/items.py index 4cfd26b..9927ef4 100644 --- a/dungeonbattle/entities/items.py +++ b/dungeonbattle/entities/items.py @@ -5,14 +5,22 @@ from ..interfaces import Entity, FightingEntity, Map class Item(Entity): + """ + A class for items + """ held: bool - held_by: Optional["Player"] + held_by: Optional[Player] - def __init__(self, *args, **kwargs): + def __init__(self, held: bool = False, held_by: Optional[Player] = None, + *args, **kwargs): super().__init__(*args, **kwargs) - self.held = False + self.held = held + self.held_by = held_by def drop(self, y: int, x: int) -> None: + """ + The item is dropped from the inventory onto the floor + """ if self.held: self.held_by.inventory.remove(self) self.held = False @@ -21,15 +29,32 @@ class Item(Entity): self.move(y, x) def hold(self, player: "Player") -> None: + """ + The item is taken from the floor and put into the inventory + """ self.held = True self.held_by = player self.map.remove_entity(self) player.inventory.append(self) + def save_state(self) -> dict: + """ + Saves the state of the entity into a dictionary + """ + d = super().save_state() + d["held"] = self.held + return d + class Heart(Item): - name: str = "heart" - healing: int = 5 + """ + A heart item to return health to the player + """ + healing: int + + def __init__(self, healing: int = 5, *args, **kwargs): + super().__init__(name="heart", *args, **kwargs) + self.healing = healing def hold(self, player: "Player") -> None: """ @@ -38,23 +63,47 @@ class Heart(Item): player.health = min(player.maxhealth, player.health + self.healing) self.map.remove_entity(self) + def save_state(self) -> dict: + """ + Saves the state of the header into a dictionary + """ + d = super().save_state() + d["healing"] = self.healing + return d + class Bomb(Item): - name: str = "bomb" + """ + A bomb item intended to deal damage to enemies at long range + """ damage: int = 5 exploding: bool - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.exploding = False + def __init__(self, damage: int = 5, exploding: bool = False, + *args, **kwargs): + super().__init__(name="bomb", *args, **kwargs) + self.damage = damage + self.exploding = exploding def drop(self, x: int, y: int) -> None: super().drop(x, y) self.exploding = True def act(self, m: Map) -> None: + """ + Special exploding action of the bomb + """ if self.exploding: - for e in m.entities: + for e in m.entities.copy(): if abs(e.x - self.x) + abs(e.y - self.y) <= 1 and \ isinstance(e, FightingEntity): e.take_damage(self, self.damage) + + def save_state(self) -> dict: + """ + Saves the state of the bomb into a dictionary + """ + d = super().save_state() + d["exploding"] = self.exploding + d["damage"] = self.damage + return d diff --git a/dungeonbattle/entities/monsters.py b/dungeonbattle/entities/monsters.py index 327521f..1f04372 100644 --- a/dungeonbattle/entities/monsters.py +++ b/dungeonbattle/entities/monsters.py @@ -5,6 +5,24 @@ from ..interfaces import FightingEntity, Map class Monster(FightingEntity): + """ + The class for all monsters in the dungeon. + A monster must override this class, and the parameters are given + in the __init__ function. + An example of the specification of a monster that has a strength of 4 + and 20 max HP: + + class MyMonster(Monster): + def __init__(self, strength: int = 4, maxhealth: int = 20, + *args, **kwargs) -> None: + super().__init__(name="my_monster", strength=strength, + maxhealth=maxhealth, *args, **kwargs) + + With that way, attributes can be overwritten when the entity got created. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + def act(self, m: Map) -> None: """ By default, a monster will move randomly where it is possible @@ -35,24 +53,40 @@ class Monster(FightingEntity): class Beaver(Monster): - name = "beaver" - maxhealth = 30 - strength = 2 + """ + A beaver monster + """ + def __init__(self, strength: int = 2, maxhealth: int = 20, + *args, **kwargs) -> None: + super().__init__(name="beaver", strength=strength, + maxhealth=maxhealth, *args, **kwargs) class Hedgehog(Monster): - name = "hedgehog" - maxhealth = 10 - strength = 3 + """ + A really mean hedgehog monster + """ + def __init__(self, strength: int = 3, maxhealth: int = 10, + *args, **kwargs) -> None: + super().__init__(name="hedgehog", strength=strength, + maxhealth=maxhealth, *args, **kwargs) class Rabbit(Monster): - name = "rabbit" - maxhealth = 15 - strength = 1 + """ + A rabbit monster + """ + def __init__(self, strength: int = 1, maxhealth: int = 15, + *args, **kwargs) -> None: + super().__init__(name="rabbit", strength=strength, + maxhealth=maxhealth, *args, **kwargs) class TeddyBear(Monster): - name = "teddy_bear" - maxhealth = 50 - strength = 0 + """ + A cute teddybear monster + """ + def __init__(self, strength: int = 0, maxhealth: int = 50, + *args, **kwargs) -> None: + super().__init__(name="teddy_bear", strength=strength, + maxhealth=maxhealth, *args, **kwargs) diff --git a/dungeonbattle/entities/player.py b/dungeonbattle/entities/player.py index c1bde5e..873da32 100644 --- a/dungeonbattle/entities/player.py +++ b/dungeonbattle/entities/player.py @@ -5,22 +5,26 @@ from ..interfaces import FightingEntity class Player(FightingEntity): - name = "player" - maxhealth: int = 20 - strength: int = 5 - intelligence: int = 1 - charisma: int = 1 - dexterity: int = 1 - constitution: int = 1 - level: int = 1 + """ + The class of the player + """ current_xp: int = 0 max_xp: int = 10 inventory: list paths: Dict[Tuple[int, int], Tuple[int, int]] - def __init__(self): - super().__init__() + def __init__(self, maxhealth: int = 20, strength: int = 5, + intelligence: int = 1, charisma: int = 1, dexterity: int = 1, + constitution: int = 1, level: int = 1, current_xp: int = 0, + max_xp: int = 10, *args, **kwargs) -> None: + super().__init__(name="player", maxhealth=maxhealth, strength=strength, + intelligence=intelligence, charisma=charisma, + dexterity=dexterity, constitution=constitution, + level=level, *args, **kwargs) + self.current_xp = current_xp + self.max_xp = max_xp self.inventory = list() + self.paths = dict() def move(self, y: int, x: int) -> None: """ @@ -100,3 +104,12 @@ class Player(FightingEntity): distances[(new_y, new_x)] = distances[(y, x)] + 1 queue.append((new_y, new_x)) self.paths = predecessors + + def save_state(self) -> dict: + """ + Saves the state of the entity into a dictionary + """ + d = super().save_state() + d["current_xp"] = self.current_xp + d["max_xp"] = self.max_xp + return d diff --git a/dungeonbattle/enums.py b/dungeonbattle/enums.py index 2a6b993..e5f73d9 100644 --- a/dungeonbattle/enums.py +++ b/dungeonbattle/enums.py @@ -3,13 +3,22 @@ from typing import Optional from dungeonbattle.settings import Settings +# This file contains a few useful enumeration classes used elsewhere in the code + class DisplayActions(Enum): + """ + Display actions options for the callable displayaction Game uses + It just calls the same action on the display object displayaction refers to. + """ REFRESH = auto() UPDATE = auto() class GameMode(Enum): + """ + Game mode options + """ MAINMENU = auto() PLAY = auto() SETTINGS = auto() @@ -17,6 +26,9 @@ class GameMode(Enum): class KeyValues(Enum): + """ + Key values options used in the game + """ UP = auto() DOWN = auto() LEFT = auto() diff --git a/dungeonbattle/game.py b/dungeonbattle/game.py index 39e7b48..9c3155e 100644 --- a/dungeonbattle/game.py +++ b/dungeonbattle/game.py @@ -1,5 +1,8 @@ from random import randint from typing import Any, Optional +import json +import os +import sys from .entities.player import Player from .enums import GameMode, KeyValues, DisplayActions @@ -10,6 +13,9 @@ from typing import Callable class Game: + """ + The game object controls all actions in the game. + """ map: Map player: Player display_actions: Callable[[DisplayActions], None] @@ -37,16 +43,11 @@ class Game: self.player.move(self.map.start_y, self.map.start_x) self.map.spawn_random_entities(randint(3, 10)) - @staticmethod - def load_game(filename: str) -> None: - # TODO loading map from a file - raise NotImplementedError() - def run(self, screen: Any) -> None: """ Main infinite loop. - We wait for a player action, then we do what that should be done - when the given key got pressed. + We wait for the player's action, then we do what that should be done + when the given key gets pressed. """ while True: # pragma no cover screen.clear() @@ -65,14 +66,14 @@ class Game: if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.MAINMENU: - self.main_menu.handle_key_pressed(key, self) + self.handle_key_pressed_main_menu(key) elif self.state == GameMode.SETTINGS: self.settings_menu.handle_key_pressed(key, raw_key, self) self.display_actions(DisplayActions.REFRESH) def handle_key_pressed_play(self, key: KeyValues) -> None: """ - In play mode, arrows or zqsd should move the main character. + In play mode, arrows or zqsd move the main character. """ if key == KeyValues.UP: if self.player.move_up(): @@ -88,3 +89,54 @@ class Game: self.map.tick() elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU + + def handle_key_pressed_main_menu(self, key: KeyValues) -> None: + """ + In the main menu, we can navigate through options. + """ + if key == KeyValues.DOWN: + self.main_menu.go_down() + if key == KeyValues.UP: + self.main_menu.go_up() + if key == KeyValues.ENTER: + option = self.main_menu.validate() + if option == menus.MainMenuValues.START: + self.state = GameMode.PLAY + elif option == menus.MainMenuValues.SAVE: + self.save_game() + elif option == menus.MainMenuValues.LOAD: + self.load_game() + elif option == menus.MainMenuValues.SETTINGS: + self.state = GameMode.SETTINGS + elif option == menus.MainMenuValues.EXIT: + sys.exit(0) + + def save_state(self) -> dict: + """ + Saves the game to a dictionary + """ + return self.map.save_state() + + def load_state(self, d: dict) -> None: + """ + Loads the game from a dictionary + """ + self.map.load_state(d) + # noinspection PyTypeChecker + self.player = self.map.find_entities(Player)[0] + self.display_actions(DisplayActions.UPDATE) + + def load_game(self) -> None: + """ + Loads the game from a file + """ + if os.path.isfile("save.json"): + with open("save.json", "r") as f: + self.load_state(json.loads(f.read())) + + def save_game(self) -> None: + """ + Saves the game to a file + """ + with open("save.json", "w") as f: + f.write(json.dumps(self.save_state())) diff --git a/dungeonbattle/interfaces.py b/dungeonbattle/interfaces.py index b057400..873678a 100644 --- a/dungeonbattle/interfaces.py +++ b/dungeonbattle/interfaces.py @@ -2,7 +2,7 @@ from enum import Enum, auto from math import sqrt from random import choice, randint -from typing import List +from typing import List, Optional from dungeonbattle.display.texturepack import TexturePack @@ -10,7 +10,7 @@ from dungeonbattle.display.texturepack import TexturePack class Map: """ Object that represents a Map with its width, height - and the whole tiles, with their custom properties. + and tiles, that have their custom properties. """ width: int height: int @@ -45,6 +45,10 @@ class Map: """ self.entities.remove(entity) + def find_entities(self, entity_class: type) -> list: + return [entity for entity in self.entities + if isinstance(entity, entity_class)] + def is_free(self, y: int, x: int) -> bool: """ Indicates that the case at the coordinates (y, x) is empty. @@ -78,6 +82,16 @@ class Map: return Map(width, height, tiles, start_y, start_x) + @staticmethod + def load_dungeon_from_string(content: str) -> List[List["Tile"]]: + """ + Transforms a string into the list of corresponding tiles + """ + lines = content.split("\n") + tiles = [[Tile.from_ascii_char(c) + for x, c in enumerate(line)] for y, line in enumerate(lines)] + return tiles + def draw_string(self, pack: TexturePack) -> str: """ Draw the current map as a string object that can be rendered @@ -108,23 +122,69 @@ class Map: for entity in self.entities: entity.act(self) + def save_state(self) -> dict: + """ + Saves the map's attributes to a dictionary + """ + d = dict() + d["width"] = self.width + d["height"] = self.height + d["start_y"] = self.start_y + d["start_x"] = self.start_x + d["currentx"] = self.currentx + d["currenty"] = self.currenty + d["entities"] = [] + for enti in self.entities: + d["entities"].append(enti.save_state()) + d["map"] = self.draw_string(TexturePack.ASCII_PACK) + return d + + def load_state(self, d: dict) -> None: + """ + Loads the map's attributes from a dictionary + """ + self.width = d["width"] + self.height = d["height"] + self.start_y = d["start_y"] + self.start_x = d["start_x"] + self.currentx = d["currentx"] + self.currenty = d["currenty"] + self.tiles = self.load_dungeon_from_string(d["map"]) + self.entities = [] + dictclasses = Entity.get_all_entity_classes_in_a_dict() + for entisave in d["entities"]: + self.add_entity(dictclasses[entisave["type"]](**entisave)) + class Tile(Enum): + """ + The internal representation of the tiles of the map + """ EMPTY = auto() WALL = auto() FLOOR = auto() - @classmethod - def from_ascii_char(cls, ch: str) -> "Tile": + @staticmethod + def from_ascii_char(ch: str) -> "Tile": + """ + Maps an ascii character to its equivalent in the texture pack + """ for tile in Tile: if tile.char(TexturePack.ASCII_PACK) == ch: return tile raise ValueError(ch) def char(self, pack: TexturePack) -> str: + """ + Translates a Tile to the corresponding character according + to the texture pack + """ return getattr(pack, self.name) def is_wall(self) -> bool: + """ + Is this Tile a wall? + """ return self == Tile.WALL def can_walk(self) -> bool: @@ -135,40 +195,65 @@ class Tile(Enum): class Entity: + """ + An Entity object represents any entity present on the map + """ y: int x: int name: str map: Map - def __init__(self): - self.y = 0 - self.x = 0 + # noinspection PyShadowingBuiltins + def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None, + map: Optional[Map] = None, *ignored, **ignored2): + self.y = y + self.x = x + self.name = name + self.map = map def check_move(self, y: int, x: int, move_if_possible: bool = False)\ -> bool: + """ + Checks if moving to (y,x) is authorized + """ free = self.map.is_free(y, x) if free and move_if_possible: self.move(y, x) return free def move(self, y: int, x: int) -> bool: + """ + Moves an entity to (y,x) coordinates + """ self.y = y self.x = x return True def move_up(self, force: bool = False) -> bool: + """ + Moves the entity up one tile, if possible + """ return self.move(self.y - 1, self.x) if force else \ self.check_move(self.y - 1, self.x, True) def move_down(self, force: bool = False) -> bool: + """ + Moves the entity down one tile, if possible + """ return self.move(self.y + 1, self.x) if force else \ self.check_move(self.y + 1, self.x, True) def move_left(self, force: bool = False) -> bool: + """ + Moves the entity left one tile, if possible + """ return self.move(self.y, self.x - 1) if force else \ self.check_move(self.y, self.x - 1, True) def move_right(self, force: bool = False) -> bool: + """ + Moves the entity right one tile, if possible + """ return self.move(self.y, self.x + 1) if force else \ self.check_move(self.y, self.x + 1, True) @@ -193,44 +278,122 @@ class Entity: return sqrt(self.distance_squared(other)) def is_fighting_entity(self) -> bool: + """ + Is this entity a fighting entity? + """ return isinstance(self, FightingEntity) def is_item(self) -> bool: + """ + Is this entity an item? + """ from dungeonbattle.entities.items import Item return isinstance(self, Item) @staticmethod def get_all_entity_classes(): + """ + Returns all entities subclasses + """ from dungeonbattle.entities.items import Heart, Bomb from dungeonbattle.entities.monsters import Beaver, Hedgehog, \ Rabbit, TeddyBear return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear] + @staticmethod + def get_all_entity_classes_in_a_dict() -> dict: + """ + Returns all entities subclasses in a dictionary + """ + from dungeonbattle.entities.player import Player + from dungeonbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \ + TeddyBear + from dungeonbattle.entities.items import Bomb, Heart + return { + "Beaver": Beaver, + "Bomb": Bomb, + "Heart": Heart, + "Hedgehog": Hedgehog, + "Rabbit": Rabbit, + "TeddyBear": TeddyBear, + "Player": Player, + } + + def save_state(self) -> dict: + """ + Saves the coordinates of the entity + """ + d = dict() + d["x"] = self.x + d["y"] = self.y + d["type"] = self.__class__.__name__ + return d + class FightingEntity(Entity): + """ + A FightingEntity is an entity that can fight, and thus has a health, + level and stats + """ maxhealth: int health: int strength: int - dead: bool intelligence: int charisma: int dexterity: int constitution: int level: int - def __init__(self): - super().__init__() - self.health = self.maxhealth - self.dead = False + def __init__(self, maxhealth: int = 0, health: Optional[int] = None, + strength: int = 0, intelligence: int = 0, charisma: int = 0, + dexterity: int = 0, constitution: int = 0, level: int = 0, + *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.maxhealth = maxhealth + self.health = maxhealth if health is None else health + self.strength = strength + self.intelligence = intelligence + self.charisma = charisma + self.dexterity = dexterity + self.constitution = constitution + self.level = level + + @property + def dead(self) -> bool: + return self.health <= 0 def hit(self, opponent: "FightingEntity") -> None: + """ + Deals damage to the opponent, based on the stats + """ opponent.take_damage(self, self.strength) def take_damage(self, attacker: "Entity", amount: int) -> None: + """ + Take damage from the attacker, based on the stats + """ self.health -= amount if self.health <= 0: self.die() def die(self) -> None: - self.dead = True + """ + If a fighting entity has no more health, it dies and is removed + """ self.map.remove_entity(self) + + def keys(self) -> list: + """ + Returns a fighting entities specific attributes + """ + return ["maxhealth", "health", "level", "strength", + "intelligence", "charisma", "dexterity", "constitution"] + + def save_state(self) -> dict: + """ + Saves the state of the entity into a dictionary + """ + d = super().save_state() + for name in self.keys(): + d[name] = getattr(self, name) + return d diff --git a/dungeonbattle/menus.py b/dungeonbattle/menus.py index af20978..a674341 100644 --- a/dungeonbattle/menus.py +++ b/dungeonbattle/menus.py @@ -1,4 +1,3 @@ -import sys from enum import Enum from typing import Any, Optional @@ -8,23 +7,40 @@ from .settings import Settings class Menu: + """ + A Menu object is the logical representation of a menu in the game + """ values: list def __init__(self): self.position = 0 def go_up(self) -> None: + """ + Moves the pointer of the menu on the previous value + """ self.position = max(0, self.position - 1) def go_down(self) -> None: + """ + Moves the pointer of the menu on the next value + """ self.position = min(len(self.values) - 1, self.position + 1) def validate(self) -> Any: + """ + Selects the value that is pointed by the menu pointer + """ return self.values[self.position] class MainMenuValues(Enum): + """ + Values of the main menu + """ START = 'Jouer' + SAVE = 'Sauvegarder' + LOAD = 'Charger' SETTINGS = 'Paramètres' EXIT = 'Quitter' @@ -33,27 +49,16 @@ class MainMenuValues(Enum): class MainMenu(Menu): + """ + A special instance of a menu : the main menu + """ values = [e for e in MainMenuValues] - def handle_key_pressed(self, key: KeyValues, game: Any) -> None: - """ - In the main menu, we can navigate through options. - """ - if key == KeyValues.DOWN: - self.go_down() - if key == KeyValues.UP: - self.go_up() - if key == KeyValues.ENTER: - option = self.validate() - if option == MainMenuValues.START: - game.state = GameMode.PLAY - elif option == MainMenuValues.SETTINGS: - game.state = GameMode.SETTINGS - elif option == MainMenuValues.EXIT: - sys.exit(0) - class SettingsMenu(Menu): + """ + A special instance of a menu : the settings menu + """ waiting_for_key: bool = False def update_values(self, settings: Settings) -> None: @@ -63,7 +68,7 @@ class SettingsMenu(Menu): def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, game: Any) -> None: """ - Update settings + In the setting menu, we van select a setting and change it """ if not self.waiting_for_key: # Navigate normally through the menu. @@ -99,9 +104,3 @@ class SettingsMenu(Menu): game.settings.write_settings() self.waiting_for_key = False self.update_values(game.settings) - - -class ArbitraryMenu(Menu): - def __init__(self, values: list): - super().__init__() - self.values = values diff --git a/dungeonbattle/term_manager.py b/dungeonbattle/term_manager.py index a425272..b1f10b1 100644 --- a/dungeonbattle/term_manager.py +++ b/dungeonbattle/term_manager.py @@ -3,6 +3,10 @@ from types import TracebackType class TermManager: # pragma: no cover + """ + The TermManager object initializes the terminal, returns a screen object and + de-initializes the terminal after use + """ def __init__(self): self.screen = curses.initscr() # convert escapes sequences to curses abstraction diff --git a/dungeonbattle/tests/entities_test.py b/dungeonbattle/tests/entities_test.py index d2c8171..b272541 100644 --- a/dungeonbattle/tests/entities_test.py +++ b/dungeonbattle/tests/entities_test.py @@ -1,7 +1,7 @@ import unittest from dungeonbattle.entities.items import Bomb, Heart, Item -from dungeonbattle.entities.monsters import Hedgehog +from dungeonbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear from dungeonbattle.entities.player import Player from dungeonbattle.interfaces import Entity, Map @@ -35,21 +35,18 @@ class TestEntities(unittest.TestCase): """ Test some random stuff with fighting entities. """ - entity = Hedgehog() + entity = Beaver() self.map.add_entity(entity) - self.assertEqual(entity.maxhealth, 10) + self.assertEqual(entity.maxhealth, 20) self.assertEqual(entity.maxhealth, entity.health) - self.assertEqual(entity.strength, 3) - self.assertIsNone(entity.hit(entity)) - self.assertFalse(entity.dead) - self.assertIsNone(entity.hit(entity)) - self.assertFalse(entity.dead) - self.assertIsNone(entity.hit(entity)) - self.assertFalse(entity.dead) + self.assertEqual(entity.strength, 2) + for _ in range(9): + self.assertIsNone(entity.hit(entity)) + self.assertFalse(entity.dead) self.assertIsNone(entity.hit(entity)) self.assertTrue(entity.dead) - entity = Hedgehog() + entity = Rabbit() self.map.add_entity(entity) entity.move(15, 44) # Move randomly @@ -61,13 +58,17 @@ class TestEntities(unittest.TestCase): self.map.tick() self.assertTrue(entity.y == 2 and entity.x == 6) - # Hedgehog should fight + # Rabbit should fight old_health = self.player.health self.map.tick() self.assertTrue(entity.y == 2 and entity.x == 6) self.assertEqual(old_health - entity.strength, self.player.health) - # Fight the hedgehog + # Fight the rabbit + old_health = entity.health + self.player.move_down() + self.assertEqual(entity.health, old_health - self.player.strength) + self.assertFalse(entity.dead) old_health = entity.health self.player.move_down() self.assertEqual(entity.health, old_health - self.player.strength) @@ -104,17 +105,25 @@ class TestEntities(unittest.TestCase): """ item = Bomb() hedgehog = Hedgehog() + teddy_bear = TeddyBear() self.map.add_entity(item) self.map.add_entity(hedgehog) + self.map.add_entity(teddy_bear) hedgehog.health = 2 + teddy_bear.health = 2 hedgehog.move(41, 42) + teddy_bear.move(42, 41) item.act(self.map) self.assertFalse(hedgehog.dead) + self.assertFalse(teddy_bear.dead) item.drop(42, 42) self.assertEqual(item.y, 42) self.assertEqual(item.x, 42) item.act(self.map) self.assertTrue(hedgehog.dead) + self.assertTrue(teddy_bear.dead) + bomb_state = item.save_state() + self.assertEqual(bomb_state["damage"], item.damage) def test_hearts(self) -> None: """ @@ -128,6 +137,8 @@ class TestEntities(unittest.TestCase): self.assertNotIn(item, self.map.entities) self.assertEqual(self.player.health, self.player.maxhealth - item.healing) + heart_state = item.save_state() + self.assertEqual(heart_state["healing"], item.healing) def test_players(self) -> None: """ @@ -158,3 +169,6 @@ class TestEntities(unittest.TestCase): self.assertEqual(player.current_xp, 10) self.assertEqual(player.max_xp, 40) self.assertEqual(player.level, 4) + + player_state = player.save_state() + self.assertEqual(player_state["current_xp"], 10) diff --git a/dungeonbattle/tests/game_test.py b/dungeonbattle/tests/game_test.py index 80720ee..6784dd2 100644 --- a/dungeonbattle/tests/game_test.py +++ b/dungeonbattle/tests/game_test.py @@ -21,8 +21,20 @@ class TestGame(unittest.TestCase): self.game.display_actions = display.handle_display_action def test_load_game(self) -> None: - self.assertRaises(NotImplementedError, Game.load_game, "game.save") - self.assertRaises(NotImplementedError, Display(None).display) + """ + Save a game and reload it. + """ + old_state = self.game.save_state() + + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SAVE) + self.game.handle_key_pressed(KeyValues.ENTER) # Save game + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertEqual(self.game.main_menu.validate(), MainMenuValues.LOAD) + self.game.handle_key_pressed(KeyValues.ENTER) # Load game + + new_state = self.game.save_state() + self.assertEqual(old_state, new_state) def test_bootstrap_fail(self) -> None: """ @@ -82,6 +94,12 @@ class TestGame(unittest.TestCase): self.assertEqual(self.game.main_menu.validate(), MainMenuValues.START) self.game.handle_key_pressed(KeyValues.DOWN) + self.assertEqual(self.game.main_menu.validate(), + MainMenuValues.SAVE) + self.game.handle_key_pressed(KeyValues.DOWN) + self.assertEqual(self.game.main_menu.validate(), + MainMenuValues.LOAD) + self.game.handle_key_pressed(KeyValues.DOWN) self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SETTINGS) self.game.handle_key_pressed(KeyValues.ENTER) @@ -100,6 +118,12 @@ class TestGame(unittest.TestCase): self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SETTINGS) self.game.handle_key_pressed(KeyValues.UP) + self.assertEqual(self.game.main_menu.validate(), + MainMenuValues.LOAD) + self.game.handle_key_pressed(KeyValues.UP) + self.assertEqual(self.game.main_menu.validate(), + MainMenuValues.SAVE) + self.game.handle_key_pressed(KeyValues.UP) self.assertEqual(self.game.main_menu.validate(), MainMenuValues.START) @@ -146,6 +170,8 @@ class TestGame(unittest.TestCase): # Open settings menu self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.DOWN) self.game.handle_key_pressed(KeyValues.ENTER) self.assertEqual(self.game.state, GameMode.SETTINGS) @@ -214,3 +240,9 @@ class TestGame(unittest.TestCase): new_y, new_x = self.game.player.y, self.game.player.x self.assertEqual(new_y, y) self.assertEqual(new_x, x) + + def test_not_implemented(self) -> None: + """ + Check that some functions are not implemented, only for coverage. + """ + self.assertRaises(NotImplementedError, Display.display, None) diff --git a/dungeonbattle/tests/menus_test.py b/dungeonbattle/tests/menus_test.py deleted file mode 100644 index 6ad9df7..0000000 --- a/dungeonbattle/tests/menus_test.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from dungeonbattle.menus import ArbitraryMenu, MainMenu, MainMenuValues - - -class TestMenus(unittest.TestCase): - def test_scroll_menu(self) -> None: - """ - Test to scroll the menu. - """ - arbitrary_menu = ArbitraryMenu([]) - self.assertEqual(arbitrary_menu.position, 0) - - main_menu = MainMenu() - self.assertEqual(main_menu.position, 0) - self.assertEqual(main_menu.validate(), MainMenuValues.START) - main_menu.go_up() - self.assertEqual(main_menu.validate(), MainMenuValues.START) - main_menu.go_down() - self.assertEqual(main_menu.validate(), MainMenuValues.SETTINGS) - main_menu.go_down() - self.assertEqual(main_menu.validate(), MainMenuValues.EXIT) - main_menu.go_down() - self.assertEqual(main_menu.validate(), MainMenuValues.EXIT) diff --git a/fix-squirrel-emojis.conf b/fix-squirrel-emojis.conf new file mode 100644 index 0000000..f47023e --- /dev/null +++ b/fix-squirrel-emojis.conf @@ -0,0 +1,118 @@ + + + + + + + + + emoji + Noto Color Emoji + + + + + + + sans + Noto Color Emoji + + + + serif + Noto Color Emoji + + + + sans-serif + Noto Color Emoji + + + + monospace + Noto Color Emoji + + + + + + + + + + Symbola + + + + + + + + + + Android Emoji + Noto Color Emoji + + + + Apple Color Emoji + Noto Color Emoji + + + + EmojiSymbols + Noto Color Emoji + + + + Emoji Two + Noto Color Emoji + + + + EmojiTwo + Noto Color Emoji + + + + Noto Color Emoji + Noto Color Emoji + + + + Segoe UI Emoji + Noto Color Emoji + + + + Segoe UI Symbol + Noto Color Emoji + + + + Symbola + Noto Color Emoji + + + + Twemoji + Noto Color Emoji + + + + Twemoji Mozilla + Noto Color Emoji + + + + TwemojiMozilla + Noto Color Emoji + + + + Twitter Color Emoji + Noto Color Emoji + + + +