Merge branch 'master' into 'settings-menu'

# Conflicts:
#   dungeonbattle/menus.py
This commit is contained in:
ynerant 2020-11-19 01:19:08 +01:00
commit cf863e8fb8
17 changed files with 661 additions and 119 deletions

3
.gitignore vendored
View File

@ -11,3 +11,6 @@ __pycache__
# Don't commit settings # Don't commit settings
settings.json settings.json
# Don't commit game save
save.json

View File

@ -3,7 +3,7 @@
# Dungeon Battle # Dungeon Battle
M1 Software engineering project Projet de génie logiciel de M1
## Création d'un environnement de développement ## 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)$ pip3 install -r requirements.txt
(env)$ deactivate # sortir de l'environnement (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
```

BIN
dungeonbattle/__init__.pyc Normal file

Binary file not shown.

View File

@ -4,6 +4,12 @@ from dungeonbattle.term_manager import TermManager
class Bootstrap: 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 @staticmethod
def run_game(): def run_game():

BIN
dungeonbattle/bootstrap.pyc Normal file

Binary file not shown.

View File

@ -5,14 +5,22 @@ from ..interfaces import Entity, FightingEntity, Map
class Item(Entity): class Item(Entity):
"""
A class for items
"""
held: bool 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) super().__init__(*args, **kwargs)
self.held = False self.held = held
self.held_by = held_by
def drop(self, y: int, x: int) -> None: def drop(self, y: int, x: int) -> None:
"""
The item is dropped from the inventory onto the floor
"""
if self.held: if self.held:
self.held_by.inventory.remove(self) self.held_by.inventory.remove(self)
self.held = False self.held = False
@ -21,15 +29,32 @@ class Item(Entity):
self.move(y, x) self.move(y, x)
def hold(self, player: "Player") -> None: def hold(self, player: "Player") -> None:
"""
The item is taken from the floor and put into the inventory
"""
self.held = True self.held = True
self.held_by = player self.held_by = player
self.map.remove_entity(self) self.map.remove_entity(self)
player.inventory.append(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): 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: def hold(self, player: "Player") -> None:
""" """
@ -38,23 +63,47 @@ class Heart(Item):
player.health = min(player.maxhealth, player.health + self.healing) player.health = min(player.maxhealth, player.health + self.healing)
self.map.remove_entity(self) 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): class Bomb(Item):
name: str = "bomb" """
A bomb item intended to deal damage to enemies at long range
"""
damage: int = 5 damage: int = 5
exploding: bool exploding: bool
def __init__(self, *args, **kwargs): def __init__(self, damage: int = 5, exploding: bool = False,
super().__init__(*args, **kwargs) *args, **kwargs):
self.exploding = False super().__init__(name="bomb", *args, **kwargs)
self.damage = damage
self.exploding = exploding
def drop(self, x: int, y: int) -> None: def drop(self, x: int, y: int) -> None:
super().drop(x, y) super().drop(x, y)
self.exploding = True self.exploding = True
def act(self, m: Map) -> None: def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
"""
if self.exploding: 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 \ if abs(e.x - self.x) + abs(e.y - self.y) <= 1 and \
isinstance(e, FightingEntity): isinstance(e, FightingEntity):
e.take_damage(self, self.damage) 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

View File

@ -5,6 +5,24 @@ from ..interfaces import FightingEntity, Map
class Monster(FightingEntity): 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: def act(self, m: Map) -> None:
""" """
By default, a monster will move randomly where it is possible By default, a monster will move randomly where it is possible
@ -35,24 +53,40 @@ class Monster(FightingEntity):
class Beaver(Monster): class Beaver(Monster):
name = "beaver" """
maxhealth = 30 A beaver monster
strength = 2 """
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): class Hedgehog(Monster):
name = "hedgehog" """
maxhealth = 10 A really mean hedgehog monster
strength = 3 """
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): class Rabbit(Monster):
name = "rabbit" """
maxhealth = 15 A rabbit monster
strength = 1 """
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): class TeddyBear(Monster):
name = "teddy_bear" """
maxhealth = 50 A cute teddybear monster
strength = 0 """
def __init__(self, strength: int = 0, maxhealth: int = 50,
*args, **kwargs) -> None:
super().__init__(name="teddy_bear", strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -5,22 +5,26 @@ from ..interfaces import FightingEntity
class Player(FightingEntity): class Player(FightingEntity):
name = "player" """
maxhealth: int = 20 The class of the player
strength: int = 5 """
intelligence: int = 1
charisma: int = 1
dexterity: int = 1
constitution: int = 1
level: int = 1
current_xp: int = 0 current_xp: int = 0
max_xp: int = 10 max_xp: int = 10
inventory: list inventory: list
paths: Dict[Tuple[int, int], Tuple[int, int]] paths: Dict[Tuple[int, int], Tuple[int, int]]
def __init__(self): def __init__(self, maxhealth: int = 20, strength: int = 5,
super().__init__() 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.inventory = list()
self.paths = dict()
def move(self, y: int, x: int) -> None: def move(self, y: int, x: int) -> None:
""" """
@ -100,3 +104,12 @@ class Player(FightingEntity):
distances[(new_y, new_x)] = distances[(y, x)] + 1 distances[(new_y, new_x)] = distances[(y, x)] + 1
queue.append((new_y, new_x)) queue.append((new_y, new_x))
self.paths = predecessors 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

View File

@ -3,13 +3,22 @@ from typing import Optional
from dungeonbattle.settings import Settings from dungeonbattle.settings import Settings
# This file contains a few useful enumeration classes used elsewhere in the code
class DisplayActions(Enum): 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() REFRESH = auto()
UPDATE = auto() UPDATE = auto()
class GameMode(Enum): class GameMode(Enum):
"""
Game mode options
"""
MAINMENU = auto() MAINMENU = auto()
PLAY = auto() PLAY = auto()
SETTINGS = auto() SETTINGS = auto()
@ -17,6 +26,9 @@ class GameMode(Enum):
class KeyValues(Enum): class KeyValues(Enum):
"""
Key values options used in the game
"""
UP = auto() UP = auto()
DOWN = auto() DOWN = auto()
LEFT = auto() LEFT = auto()

View File

@ -1,5 +1,8 @@
from random import randint from random import randint
from typing import Any, Optional from typing import Any, Optional
import json
import os
import sys
from .entities.player import Player from .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions from .enums import GameMode, KeyValues, DisplayActions
@ -10,6 +13,9 @@ from typing import Callable
class Game: class Game:
"""
The game object controls all actions in the game.
"""
map: Map map: Map
player: Player player: Player
display_actions: Callable[[DisplayActions], None] display_actions: Callable[[DisplayActions], None]
@ -37,16 +43,11 @@ class Game:
self.player.move(self.map.start_y, self.map.start_x) self.player.move(self.map.start_y, self.map.start_x)
self.map.spawn_random_entities(randint(3, 10)) 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: def run(self, screen: Any) -> None:
""" """
Main infinite loop. Main infinite loop.
We wait for a player action, then we do what that should be done We wait for the player's action, then we do what that should be done
when the given key got pressed. when the given key gets pressed.
""" """
while True: # pragma no cover while True: # pragma no cover
screen.clear() screen.clear()
@ -65,14 +66,14 @@ class Game:
if self.state == GameMode.PLAY: if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key) self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU: 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: elif self.state == GameMode.SETTINGS:
self.settings_menu.handle_key_pressed(key, raw_key, self) self.settings_menu.handle_key_pressed(key, raw_key, self)
self.display_actions(DisplayActions.REFRESH) self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None: 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 key == KeyValues.UP:
if self.player.move_up(): if self.player.move_up():
@ -88,3 +89,54 @@ class Game:
self.map.tick() self.map.tick()
elif key == KeyValues.SPACE: elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU 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()))

View File

@ -2,7 +2,7 @@
from enum import Enum, auto from enum import Enum, auto
from math import sqrt from math import sqrt
from random import choice, randint from random import choice, randint
from typing import List from typing import List, Optional
from dungeonbattle.display.texturepack import TexturePack from dungeonbattle.display.texturepack import TexturePack
@ -10,7 +10,7 @@ from dungeonbattle.display.texturepack import TexturePack
class Map: class Map:
""" """
Object that represents a Map with its width, height 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 width: int
height: int height: int
@ -45,6 +45,10 @@ class Map:
""" """
self.entities.remove(entity) 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: def is_free(self, y: int, x: int) -> bool:
""" """
Indicates that the case at the coordinates (y, x) is empty. 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) 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: def draw_string(self, pack: TexturePack) -> str:
""" """
Draw the current map as a string object that can be rendered Draw the current map as a string object that can be rendered
@ -108,23 +122,69 @@ class Map:
for entity in self.entities: for entity in self.entities:
entity.act(self) 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): class Tile(Enum):
"""
The internal representation of the tiles of the map
"""
EMPTY = auto() EMPTY = auto()
WALL = auto() WALL = auto()
FLOOR = auto() FLOOR = auto()
@classmethod @staticmethod
def from_ascii_char(cls, ch: str) -> "Tile": def from_ascii_char(ch: str) -> "Tile":
"""
Maps an ascii character to its equivalent in the texture pack
"""
for tile in Tile: for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch: if tile.char(TexturePack.ASCII_PACK) == ch:
return tile return tile
raise ValueError(ch) raise ValueError(ch)
def char(self, pack: TexturePack) -> str: def char(self, pack: TexturePack) -> str:
"""
Translates a Tile to the corresponding character according
to the texture pack
"""
return getattr(pack, self.name) return getattr(pack, self.name)
def is_wall(self) -> bool: def is_wall(self) -> bool:
"""
Is this Tile a wall?
"""
return self == Tile.WALL return self == Tile.WALL
def can_walk(self) -> bool: def can_walk(self) -> bool:
@ -135,40 +195,65 @@ class Tile(Enum):
class Entity: class Entity:
"""
An Entity object represents any entity present on the map
"""
y: int y: int
x: int x: int
name: str name: str
map: Map map: Map
def __init__(self): # noinspection PyShadowingBuiltins
self.y = 0 def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
self.x = 0 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)\ def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool: -> bool:
"""
Checks if moving to (y,x) is authorized
"""
free = self.map.is_free(y, x) free = self.map.is_free(y, x)
if free and move_if_possible: if free and move_if_possible:
self.move(y, x) self.move(y, x)
return free return free
def move(self, y: int, x: int) -> bool: def move(self, y: int, x: int) -> bool:
"""
Moves an entity to (y,x) coordinates
"""
self.y = y self.y = y
self.x = x self.x = x
return True return True
def move_up(self, force: bool = False) -> bool: 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 \ return self.move(self.y - 1, self.x) if force else \
self.check_move(self.y - 1, self.x, True) self.check_move(self.y - 1, self.x, True)
def move_down(self, force: bool = False) -> bool: 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 \ return self.move(self.y + 1, self.x) if force else \
self.check_move(self.y + 1, self.x, True) self.check_move(self.y + 1, self.x, True)
def move_left(self, force: bool = False) -> bool: 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 \ return self.move(self.y, self.x - 1) if force else \
self.check_move(self.y, self.x - 1, True) self.check_move(self.y, self.x - 1, True)
def move_right(self, force: bool = False) -> bool: 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 \ return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True) self.check_move(self.y, self.x + 1, True)
@ -193,44 +278,122 @@ class Entity:
return sqrt(self.distance_squared(other)) return sqrt(self.distance_squared(other))
def is_fighting_entity(self) -> bool: def is_fighting_entity(self) -> bool:
"""
Is this entity a fighting entity?
"""
return isinstance(self, FightingEntity) return isinstance(self, FightingEntity)
def is_item(self) -> bool: def is_item(self) -> bool:
"""
Is this entity an item?
"""
from dungeonbattle.entities.items import Item from dungeonbattle.entities.items import Item
return isinstance(self, Item) return isinstance(self, Item)
@staticmethod @staticmethod
def get_all_entity_classes(): def get_all_entity_classes():
"""
Returns all entities subclasses
"""
from dungeonbattle.entities.items import Heart, Bomb from dungeonbattle.entities.items import Heart, Bomb
from dungeonbattle.entities.monsters import Beaver, Hedgehog, \ from dungeonbattle.entities.monsters import Beaver, Hedgehog, \
Rabbit, TeddyBear Rabbit, TeddyBear
return [Beaver, Bomb, Heart, 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): class FightingEntity(Entity):
"""
A FightingEntity is an entity that can fight, and thus has a health,
level and stats
"""
maxhealth: int maxhealth: int
health: int health: int
strength: int strength: int
dead: bool
intelligence: int intelligence: int
charisma: int charisma: int
dexterity: int dexterity: int
constitution: int constitution: int
level: int level: int
def __init__(self): def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
super().__init__() strength: int = 0, intelligence: int = 0, charisma: int = 0,
self.health = self.maxhealth dexterity: int = 0, constitution: int = 0, level: int = 0,
self.dead = False *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: def hit(self, opponent: "FightingEntity") -> None:
"""
Deals damage to the opponent, based on the stats
"""
opponent.take_damage(self, self.strength) opponent.take_damage(self, self.strength)
def take_damage(self, attacker: "Entity", amount: int) -> None: def take_damage(self, attacker: "Entity", amount: int) -> None:
"""
Take damage from the attacker, based on the stats
"""
self.health -= amount self.health -= amount
if self.health <= 0: if self.health <= 0:
self.die() self.die()
def die(self) -> None: 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) 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

View File

@ -1,4 +1,3 @@
import sys
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional
@ -8,23 +7,40 @@ from .settings import Settings
class Menu: class Menu:
"""
A Menu object is the logical representation of a menu in the game
"""
values: list values: list
def __init__(self): def __init__(self):
self.position = 0 self.position = 0
def go_up(self) -> None: def go_up(self) -> None:
"""
Moves the pointer of the menu on the previous value
"""
self.position = max(0, self.position - 1) self.position = max(0, self.position - 1)
def go_down(self) -> None: 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) self.position = min(len(self.values) - 1, self.position + 1)
def validate(self) -> Any: def validate(self) -> Any:
"""
Selects the value that is pointed by the menu pointer
"""
return self.values[self.position] return self.values[self.position]
class MainMenuValues(Enum): class MainMenuValues(Enum):
"""
Values of the main menu
"""
START = 'Jouer' START = 'Jouer'
SAVE = 'Sauvegarder'
LOAD = 'Charger'
SETTINGS = 'Paramètres' SETTINGS = 'Paramètres'
EXIT = 'Quitter' EXIT = 'Quitter'
@ -33,27 +49,16 @@ class MainMenuValues(Enum):
class MainMenu(Menu): class MainMenu(Menu):
"""
A special instance of a menu : the main menu
"""
values = [e for e in MainMenuValues] 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): class SettingsMenu(Menu):
"""
A special instance of a menu : the settings menu
"""
waiting_for_key: bool = False waiting_for_key: bool = False
def update_values(self, settings: Settings) -> None: 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, def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None: game: Any) -> None:
""" """
Update settings In the setting menu, we van select a setting and change it
""" """
if not self.waiting_for_key: if not self.waiting_for_key:
# Navigate normally through the menu. # Navigate normally through the menu.
@ -99,9 +104,3 @@ class SettingsMenu(Menu):
game.settings.write_settings() game.settings.write_settings()
self.waiting_for_key = False self.waiting_for_key = False
self.update_values(game.settings) self.update_values(game.settings)
class ArbitraryMenu(Menu):
def __init__(self, values: list):
super().__init__()
self.values = values

View File

@ -3,6 +3,10 @@ from types import TracebackType
class TermManager: # pragma: no cover 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): def __init__(self):
self.screen = curses.initscr() self.screen = curses.initscr()
# convert escapes sequences to curses abstraction # convert escapes sequences to curses abstraction

View File

@ -1,7 +1,7 @@
import unittest import unittest
from dungeonbattle.entities.items import Bomb, Heart, Item 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.entities.player import Player
from dungeonbattle.interfaces import Entity, Map from dungeonbattle.interfaces import Entity, Map
@ -35,21 +35,18 @@ class TestEntities(unittest.TestCase):
""" """
Test some random stuff with fighting entities. Test some random stuff with fighting entities.
""" """
entity = Hedgehog() entity = Beaver()
self.map.add_entity(entity) self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 10) self.assertEqual(entity.maxhealth, 20)
self.assertEqual(entity.maxhealth, entity.health) self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 3) self.assertEqual(entity.strength, 2)
self.assertIsNone(entity.hit(entity)) for _ in range(9):
self.assertFalse(entity.dead) self.assertIsNone(entity.hit(entity))
self.assertIsNone(entity.hit(entity)) self.assertFalse(entity.dead)
self.assertFalse(entity.dead)
self.assertIsNone(entity.hit(entity))
self.assertFalse(entity.dead)
self.assertIsNone(entity.hit(entity)) self.assertIsNone(entity.hit(entity))
self.assertTrue(entity.dead) self.assertTrue(entity.dead)
entity = Hedgehog() entity = Rabbit()
self.map.add_entity(entity) self.map.add_entity(entity)
entity.move(15, 44) entity.move(15, 44)
# Move randomly # Move randomly
@ -61,13 +58,17 @@ class TestEntities(unittest.TestCase):
self.map.tick() self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
# Hedgehog should fight # Rabbit should fight
old_health = self.player.health old_health = self.player.health
self.map.tick() self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health) 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 old_health = entity.health
self.player.move_down() self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength) self.assertEqual(entity.health, old_health - self.player.strength)
@ -104,17 +105,25 @@ class TestEntities(unittest.TestCase):
""" """
item = Bomb() item = Bomb()
hedgehog = Hedgehog() hedgehog = Hedgehog()
teddy_bear = TeddyBear()
self.map.add_entity(item) self.map.add_entity(item)
self.map.add_entity(hedgehog) self.map.add_entity(hedgehog)
self.map.add_entity(teddy_bear)
hedgehog.health = 2 hedgehog.health = 2
teddy_bear.health = 2
hedgehog.move(41, 42) hedgehog.move(41, 42)
teddy_bear.move(42, 41)
item.act(self.map) item.act(self.map)
self.assertFalse(hedgehog.dead) self.assertFalse(hedgehog.dead)
self.assertFalse(teddy_bear.dead)
item.drop(42, 42) item.drop(42, 42)
self.assertEqual(item.y, 42) self.assertEqual(item.y, 42)
self.assertEqual(item.x, 42) self.assertEqual(item.x, 42)
item.act(self.map) item.act(self.map)
self.assertTrue(hedgehog.dead) 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: def test_hearts(self) -> None:
""" """
@ -128,6 +137,8 @@ class TestEntities(unittest.TestCase):
self.assertNotIn(item, self.map.entities) self.assertNotIn(item, self.map.entities)
self.assertEqual(self.player.health, self.assertEqual(self.player.health,
self.player.maxhealth - item.healing) self.player.maxhealth - item.healing)
heart_state = item.save_state()
self.assertEqual(heart_state["healing"], item.healing)
def test_players(self) -> None: def test_players(self) -> None:
""" """
@ -158,3 +169,6 @@ class TestEntities(unittest.TestCase):
self.assertEqual(player.current_xp, 10) self.assertEqual(player.current_xp, 10)
self.assertEqual(player.max_xp, 40) self.assertEqual(player.max_xp, 40)
self.assertEqual(player.level, 4) self.assertEqual(player.level, 4)
player_state = player.save_state()
self.assertEqual(player_state["current_xp"], 10)

View File

@ -21,8 +21,20 @@ class TestGame(unittest.TestCase):
self.game.display_actions = display.handle_display_action self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None: 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: def test_bootstrap_fail(self) -> None:
""" """
@ -82,6 +94,12 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.main_menu.validate(), self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START) MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.DOWN) 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(), self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS) MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
@ -100,6 +118,12 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.main_menu.validate(), self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS) MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.UP) 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(), self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START) MainMenuValues.START)
@ -146,6 +170,8 @@ class TestGame(unittest.TestCase):
# Open settings menu # 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.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS) 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 new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y) self.assertEqual(new_y, y)
self.assertEqual(new_x, x) 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)

View File

@ -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)

118
fix-squirrel-emojis.conf Normal file
View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Add generic family -->
<match target="pattern">
<test qual="any" name="family"><string>emoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<!-- Set as final fallback for default families -->
<match target="pattern">
<test name="family"><string>sans</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test name="family"><string>serif</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test name="family"><string>monospace</string></test>
<edit name="family" mode="append"><string>Noto Color Emoji</string></edit>
</match>
<!-- Block Symbola from being a fallback -->
<selectfont>
<rejectfont>
<pattern>
<patelt name="family">
<string>Symbola</string>
</patelt>
</pattern>
</rejectfont>
</selectfont>
<!-- Use this font when other popular ones are specifically requested -->
<match target="pattern">
<test qual="any" name="family"><string>Android Emoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Apple Color Emoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>EmojiSymbols</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Emoji Two</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>EmojiTwo</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Noto Color Emoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Segoe UI Emoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Segoe UI Symbol</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Symbola</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Twemoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Twemoji Mozilla</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>TwemojiMozilla</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
<match target="pattern">
<test qual="any" name="family"><string>Twitter Color Emoji</string></test>
<edit name="family" mode="assign" binding="same"><string>Noto Color Emoji</string></edit>
</match>
</fontconfig>