Merge branch 'master' into 'settings-menu'
# Conflicts: # dungeonbattle/menus.py
This commit is contained in:
commit
cf863e8fb8
|
@ -11,3 +11,6 @@ __pycache__
|
||||||
|
|
||||||
# Don't commit settings
|
# Don't commit settings
|
||||||
settings.json
|
settings.json
|
||||||
|
|
||||||
|
# Don't commit game save
|
||||||
|
save.json
|
||||||
|
|
69
README.md
69
README.md
|
@ -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
|
||||||
|
```
|
||||||
|
|
Binary file not shown.
|
@ -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():
|
||||||
|
|
Binary file not shown.
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
|
@ -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>
|
Loading…
Reference in New Issue