Merge branch 'master' into map_generation

# Conflicts:
#	squirrelbattle/game.py
#	squirrelbattle/interfaces.py
#	squirrelbattle/tests/game_test.py
This commit is contained in:
Yohann D'ANELLO 2021-01-08 17:02:10 +01:00
commit 1ab63434f6
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
49 changed files with 2848 additions and 593 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
env/
venv/
local/
.coverage
.pytest_cache/

View File

@ -3,21 +3,16 @@ stages:
- quality-assurance
- build
py37:
stage: test
image: python:3.7-alpine
before_script:
- apk add --no-cache gettext
- pip install tox
script: tox -e py3
py38:
py310:
stage: test
image: python:3.8-alpine
image: python:rc-alpine
before_script:
- apk add --no-cache gettext
- pip install tox
script: tox -e py3
only:
- master
py39:
@ -28,6 +23,38 @@ py39:
- pip install tox
script: tox -e py3
py38:
stage: test
image: python:3.8-alpine
before_script:
- apk add --no-cache gettext
- pip install tox
script: tox -e py3
py37:
stage: test
image: python:3.7-alpine
before_script:
- apk add --no-cache gettext
- pip install tox
script: tox -e py3
only:
- master
py36:
stage: test
image: python:3.6-alpine
before_script:
- apk add --no-cache gettext
- pip install tox
script: tox -e py3
only:
- master
linters:
stage: quality-assurance
image: python:3-alpine
@ -36,11 +63,15 @@ linters:
script: tox -e linters
allow_failure: true
build-deb:
image: debian:buster-slim
stage: build
before_script:
- apt-get update && apt-get -y --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools
- >
apt-get update && apt-get -y --no-install-recommends install
build-essential debmake dh-python debhelper gettext python3-all
python3-setuptools
script:
- dpkg-buildpackage
- mkdir build && cp ../*.deb build/

View File

@ -1,6 +1,7 @@
[![pipeline status](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/pipeline.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master)
[![coverage report](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/coverage.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master)
[![Documentation Status](https://readthedocs.org/projects/squirrel-battle/badge/?version=latest)](https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
[![PyPI](https://img.shields.io/pypi/v/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
[![PYPI downloads](https://img.shields.io/pypi/dm/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
[![AUR version](https://img.shields.io/aur/version/python-squirrel-battle)](https://aur.archlinux.org/packages/python-squirrel-battle/)

View File

@ -275,7 +275,7 @@ Construction du paquet Debian
-----------------------------
Structure du paquet
-------------------
~~~~~~~~~~~~~~~~~~~
L'ensemble des instructions pour construire le paquet Debian est situé dans le
dossier ``debian/``.
@ -292,14 +292,14 @@ Le paquet ``fonts-noto-color-emoji`` est en dépendance pour le bon affichage
des émojis.
Mettre à jour le paquet
-----------------------
~~~~~~~~~~~~~~~~~~~~~~~
Pour changer la version du paquet, il faut ajouter des lignes dans le fichier
``changelog``.
Construire le paquet
--------------------
~~~~~~~~~~~~~~~~~~~~
Il faut partir d'une installation de Debian.

View File

@ -1,4 +1,4 @@
Affichage de l'historique
=========================
Pas encore documenté.
L'historique des actions est affiché en bas de l'écran. À chaque action d'une entité, comme frapper quelqu'un, ou lorsque le joueur parle à une entité, cela s'affiche dans l'historique.

View File

@ -1,4 +1,6 @@
Affichage de la carte
=====================
Pas encore documenté.
La carte s'affiche dans la partie en haut à gauche de l'écran, sur la plus grande partie de l'écran. On affiche les tuiles une par une, selon le texture pack sélectionné. La map est actualisée à chaque action d'une entité.
L'afffichage de la carte suit les déplacements du joueur.

View File

@ -1,4 +1,14 @@
Affichage des menus
===================
Pas encore documenté.
Les menus sont affichés dans une boîte. On peut naviguer dedans avec les flèches haut et bas,
et valider avec la touche entrée.
Il y a plusieurs menus dans le jeu :
* Le main menu, qui s'affiche au lancement du jeu.
* Le menu des paramètres : si on sélectionne un choix de touche et qu'on appuie sur entrée, on peut ensuite appuyer sur une touche pour remplacer la touche utilisée.
* Le menu des crédits : ce menu fonctionne avec la souris. En cliquant on affiche une image.
* Le menu d'inventaire : dans l'inventaire, on peut utiliser les touches pour utiliser un item ou l'équiper...
* Le menu de vente : on peut utiliser les touches gauche et droite pour switcher entre l'inventaire du joueur et celui du marchand.
* Menu des warnings : Pas vraiment un menu, mais affiche juste un message dans une petite boite pour prévenir le joueur que quelquechose ne va pas.

View File

@ -1,4 +1,26 @@
Affichage des statistiques
==========================
Pas encore documenté.
.. _Hazel: ../index.html
Les statistiques du joueur sont affichées en haut à droite de l'écran
et séparées du reste de l'affichage par une barre verticale.
Les informations affichées sont :
* **LVL** - le niveau du joueur
* **EXP** - la quantité d'expérience que le joueur a gagné et combien il lui en faut avant le prochain niveau.
* **HP** - la quantité de vie que le joueur a actuellement et combien il peut en avoir au maximum.
* **STR** - la force du joueur.
* **INT** - l'intelligence du joueur.
* **CHR** - le charisme du joueur.
* **DEX** - la dextérité du joueur.
* **CON** - la constitution du joueur.
* **CRI** - le pourcentage de chance de coup critique.
* **Inventory** - le contenu de l'inventaire du joueur.
* **Equipped main** - l'objet équipé dans la main principale.
* **Equipped secondary** - l'objet équipé dans la main secondaire.
* **Equipped armor** - le plastron porté par le joueur.
* **Equipped helmet** - le casque porté par le joueur.
* **Hazel** - le nombre d'Hazel_ que le joueur possède.
* **Vous êtes mort** - Éventuellement, si le joueur est mort.

View File

@ -0,0 +1,70 @@
Entités pacifiques
==================
.. _`entité attaquante`: index.html#entites-attaquantes
.. _`pack de textures`: ../texture-pack.html
Chaque entité pacifique est en particulier une `entité attaquante`_,
et hérite donc de ses attributs, et peut alors être attaquée.
Ils sont cependant non-hostiles.
Il est possible d'interagir avec ces entités. En s'approchant d'elles, en
appuyant sur la touche ``T`` suivie de la direction où regarder, un échange
débute.
Si l'on s'adresse à un marchand, on devrait voir à l'écran l'inventaire du joueur
et l'inventaire du marchand. Les flèches haut et bas permettent de sélectionner
un objet, les touches droite et gauche de passer d'un inventaire à l'autre, et la
touche entrée valide l'action.
On dénombre actuellement 3 types d'entités pacifiques :
Tournesol
---------
Son nom est fixé à `sunflower`. Il a par défaut une **15** points de vie.
Interagir avec un tournesol n'a pas de réel intérêt, si ce n'est déclencher
le « pouvoir des fleurs !! » ou bien savoir que « le soleil est chaud
aujourd'hui ».
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``I``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🌻``.
Marchand
--------
Son nom est fixé à `merchant`. Il a par défaut **5** points de vie.
En interagissant avec un marchand, il est possible de lui acheter et de lui
vendre différents objets contre des Hazels, la monnaie du jeu.
Les prix sont fixés :
* Bombe : 4 Hazels
* Coeur : 3 Hazels
* Potion d'arrachage de corps : 14 Hazels
* Épée : 20 Hazels
* Bouclier : 16 Hazels
* Casque : 18 Hazels
* Plastron : 30 Hazels
Le marchand commence avec 75 Hazels en sa possession, contre 42 pour le joueur.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``M``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦜``.
Trompette
---------
Son nom est fixé à 'trumpet'. Une trompette est un familier, c'est à dire que
c'est une entité attaquante qui suit globalement le joueurs et attaque les monstres
qui se rapprochent trop du joueur.
Elle a 20 point de vie et une attaque de 3.
Dans le `pack de textures`_ ASCII, elle est représentée par le caractère ``/``.
Dans le `pack de textures`_ écureuil, elle est représentée par l'émoji ``🎺``.

View File

@ -7,6 +7,7 @@ Entités
player
monsters
friendly
items
Entité
@ -38,11 +39,12 @@ Entité attaquante
-----------------
.. _monstre: monsters.html
.. _entité pacifique: friendly.html
.. _joueur: player.html
Une entité attaquante (``FightingEntity``) est un type d'entités représentant
les personnages présents sur la carte, pouvant alors se battre. Ce peut être
un monstre_ ou bien le joueur_.
un monstre_, une `entité pacifique`_ ou bien le joueur_.
Elles disposent toutes, en plus des paramètres d'entité, des attributs suivants :
@ -77,3 +79,14 @@ en-dessous de 0 point de vie. À ce moment-là, l'entité est retirée de la car
Lorsqu'une entité en frappe une autre, celle-ci inflige autant de dégâts qu'elle
n'a de force, et autant de points de vie sont perdus.
Entité pacifique
----------------
Une entité pacifique (``FriendlyEntity``) est un cas particulier d'entité
attaquante. Contrairement aux montres, elles ne peuvent pas attaquer le joueur.
On peut parler à une entité pacifique en appuyant sur la touche ``T`` puis en
appuyant sur la direction dans laquelle on veut parler à l'entité.

View File

@ -19,7 +19,7 @@ Un objet dispose de deux paramètres :
Si l'objet est dans l'inventaire, renvoie son propriétaire.
Deux types d'objets sont pour l'instant présents :
Il y a plusieurs types d'objets :
Bombe
@ -28,23 +28,94 @@ Bombe
.. _entités attaquantes: index.html#entite-attaquante
Une bombe est un objet que l'on peut ramasser. Une fois ramassée, elle est placée
dans l'inventaire. Le joueur peut ensuite lâcher la bombe, qui fera alors
3 dégâts à toutes les `entités attaquantes`_ situées à moins de une case.
dans l'inventaire. Le joueur peut ensuite utiliser la bombe, via l'inventaire
ou après l'avoir équipée, qui fera alors 3 dégâts à toutes les
`entités attaquantes`_ situées à moins de trois cases au bout de 4 ticks de jeu.
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ``o``
et dans le `pack de textures`_ écureuil par l'émoji ``💣``.
et dans le `pack de textures`_ écureuil par l'émoji ``💣``. Lors de l'explosion,
la bombe est remplacée par un symbole ``%`` ou l'émoji ``💥`` selon le pack de
textures utilisé.
.. note::
La gestion de l'inventaire n'ayant pas encore été implémentée, il n'est à
l'heure actuelle pas possible de lancer une bombe.
La bombe coûte 4 Hazels auprès des marchands.
Cœur
----
Une cœur est un objet que l'on ne peut pas ramasser. Dès que le joueur s'en
approche, il est régénéré automatiquement de 3 points de vie, et le cœur disparaît.
Un cœur est un objet que l'on ne peut pas ramasser. Dès que le joueur s'en
approche ou qu'il l'achète auprès d'un marchand, il est régénéré automatiquement
de 3 points de vie, et le cœur disparaît.
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ````
Il est représenté dans le `pack de textures`_ ASCII par le caractère ````
et dans le `pack de textures`_ écureuil par l'émoji ``💜``.
Le cœur coûte 3 Hazels auprès des marchands.
Potion d'arrachage de corps
---------------------------
Cette potion permet, une fois utilisée, d'échanger toutes ses caractéristiques
avec une autre entité aléatoire sur la carte. Cela inclut la force, la position,
l'icône, ...
Elle est représentée par les caractères ``I`` et ``🔀``
Cette potion coûte 14 Hazels auprès des marchands.
Épée
----
L'épée est un objet que l'on peut trouver uniquement par achat auprès d'un
marchand pour le coût de 20 Hazels. Une fois équipée, l'épée ajoute 3 de force
à son porteur.
Elle est représentée par les caractères ```` et ``🗡️``.
Bouclier
--------
Le bouclier est un objet que l'on peut trouver uniquement par achat auprès d'un
marchand pour le coût de 16 Hazels. Il s'équippe dans la main secondaire.
Une fois équipé, le bouclier ajoute 1 de
constitution à son porteur, lui permettant de parer mieux les coups.
Il est représenté par les caractères ``D`` et ``🛡️``.
Casque
------
Le casque est un objet que l'on peut trouver uniquement par achat auprès d'un
marchand pour le coût de 18 Hazels. Il s'équippe sur la tête.
Une fois équipé, le casque ajoute 2 de
constitution à son porteur, lui permettant de prendre moins de dêgats.
Il est représenté par les caractères ``0`` et ``⛑️``.
Plastron
--------
Le plastron est un objet que l'on peut trouver uniquement par achat auprès d'un
marchand pour le coût de 30 Hazels. Il s'équippe sur le corps.
Une fois équipé, le casque ajoute 4 de
constitution à son porteur, lui permettant de prendre moins de dêgats.
Il est représenté par les caractères ``(`` et ``🦺``.
Anneau
------
L'anneau est un objet que l'on peut trouver uniquement par achat auprès d'un
marchand. Il s'équippe sur la main secondaire.
Une fois équipé, l'anneau ajoute un bonus à une ou plusieurs statistiques du
joueur, améliorant sa capacité à se débarasser des monstres.
Il y a plusieurs types d'anneaux :
* **Anneau de coup critique**, qui augmente la chance de coup critique de 20%. Il coute 15 Hazels.
* **Anneau de gain d'expérience amélioré**, qui multiplie le gain d'expérience du joueur par 2. Il coûte 25 Hazels.
Un anneau est représenté par les caractères ``o`` et ``💍``.

View File

@ -13,7 +13,7 @@ au plus vite sur le joueur pour le frapper selon l'algorithme de Dijkstra,
et s'il est suffisamment proche frappe le joueur et lui fait autant de dégâts
qu'il n'a de force.
On dénombre actuellement 4 types de monstres :
On dénombre actuellement 5 types de monstres :
Hérisson
--------
@ -40,6 +40,8 @@ Lapin
Son nom est fixé à `rabbit`. Il a par défaut une force à **1** et **15** points de vie.
Il a une chance de coup critique de 30%.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``Y``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐇``.
@ -53,3 +55,14 @@ Son nom est fixé à `teddy_bear`. Il n'a pas de force et **50** points de vie.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``8``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🧸``.
Pygargue
---------
Son nom est fixé à `eagle`. Il a par défaut une force à **1000** et **5000** points de vie.
Il s'agit d'un boss difficilement tuable, qui apparait plus rarement que les autres monstres.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``µ``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦅``.

View File

@ -17,6 +17,10 @@ Bienvenue dans la documentation de Squirrel Battle !
:target: https://pypi.org/project/squirrel-battle/
:alt: PyPI
.. image:: https://img.shields.io/pypi/pyversions/squirrel-battle
:target: https://pypi.org/project/squirrel-battle/
:alt: Supported Python versions
.. image:: https://img.shields.io/pypi/dm/squirrel-battle
:target: https://pypi.org/project/squirrel-battle/
:alt: PyPI downloads

View File

@ -1,2 +1,2 @@
sphinx
sphinx-rtd-theme
sphinx>=3.3
sphinx-rtd-theme>=0.5

View File

@ -1,4 +1,38 @@
Paramètres
==========
Pas encore documenté.
.. _pack de textures: texture-pack.html
Il est possible de changer les touches utilisées dans le jeu dans le menu des paramètres.
On peut aussi changer le `pack de textures`_ utilisé.
Touches
-------
Les touches utilisées de base sont :
* **Aller vers le haut** : z
* **Aller vers le haut (secondaire)** : ↑
* **Aller vers le bas** : s
* **Aller vers le bas (secondaire)** : ↓
* **Aller à droite** : d
* **Aller à droite (secondaire)** : →
* **Aller à gauche** : q
* **Aller à gauche (secondaire)** : ←
* **Valider le choix** : Entrée
* **Inventaire** : i
* **Utiliser un objet** : u
* **Équiper un objet** : e
* **Lacher un objet** : r
* **Parler** : t
* **Attendre** : w
Autres
------
.. _ascii: texture-pack.html#Pack ASCII
.. _squirrel: texture-pack.html#Pack Écureuil
* **Texture pack utilisé** : parmi ascii_ et squirrel_
* **Langue utilisée** : parmi anglais, français, espagnol, allemand

View File

@ -9,18 +9,30 @@ Pack de textures
.. _Joueur: entities/player.html
.. _Hérisson: entities/monsters.html#herisson
.. _Cœur: entities/items.html#coeur
.. _Bombe: entities/items.html#bombe
.. _Lapin: entities/monsters.html#lapin
.. _Tigre: entities/monsters.html#tigre
.. _Nounours: entities/monsters.html#nounours
.. _Tournesol: entities/friendly.html#tournesol
.. _Marchand: entities/friendly.html#marchand
.. _Cœur: entities/items.html#coeur
.. _Bombe: entities/items.html#bombe
.. _Explosion: entities/items.html#bombe
.. _Potion d'arrachage de corps: entities/items.html
.. _Épée: entities/items.html#epee
.. _Bouclier: entities/items.html#bouclier
.. _Hazel: ../index.html
.. _Plastron: ../entities/items.html#plastron
.. _Pygargue: ../entities/monsters.html#Pygargue
.. _Casque: ../entities/items.html#Casque
.. _Anneau: ../entities/items.html#Anneau
.. _Trompette: ../entities/items.html#Trompette
Chaque entité_ et chaque tuile_ de la carte_ est associé à un caractère pour
être affiché dans le terminal. Cependant, afin de pouvoir proposer plusieurs
expériences graphiques (notamment en fonction du support des émojis), différents
packs de textures sont proposés.
Il est possible de changer de pack dans les paramètres.
Il est possible de changer de pack dans les paramètres_.
Les packs de textures peuvent influencer la taille que prennent les tuiles_,
en raison du fait que les émojis ne sont pas monospace.
@ -39,11 +51,23 @@ Chaque tuile fait un caractère de large.
* Entités
* Joueur_ : ``@``
* Hérisson_ : ``*``
* Cœur_ : ````
* Bombe_ : ``o``
* Lapin_ : ``Y``
* Tigre_ : ``n``
* Nounours_ : ``8``
* Tournesol_ : ``I``
* Marchand_ : ``M``
* Cœur_ : ````
* Bombe_ : ``o``
* Explosion_ : ``%``
* `Potion d'arrachage de corps`_ : ``S``
* Épée_ : ````
* Bouclier_ : ``D``
* Hazel_ : ``¤``
* Plastron_ : ``(``
* Pygargue_ : ``µ``
* Casque_ : ``0``
* Anneau_ : ``o``
* Trompette_ : ``/``
Pack Écureuil
@ -58,8 +82,20 @@ Chaque tuile fait 2 caractères de large pour afficher les émojis proprement.
* Entités
* Joueur_ : ``🐿``
* Hérisson_ : ``🦔``
* Cœur_ : ``💜``
* Bombe_ : ``💣``
* Lapin_ : ``🐇``
* Tigre_ : ``🐅``
* Nounours_ : ``🧸``
* Tournesol_ : ``🌻``
* Marchand_ : ``🦜``
* Cœur_ : ``💜``
* Bombe_ : ``💣``
* Explosion_ : ``💥``
* `Potion d'arrachage de corps`_ : ``🔀``
* Épée_ : ``🗡️``
* Bouclier_ : ``🛡️``
* Hazel_ : ``🌰``
* Plastron_ : ``🦺``
* Pygargue_ : ``🦅``
* Casque_ : ``⛑️``
* Anneau_ : ``💍``
* Trompette_ : ``🎺``

View File

@ -0,0 +1,44 @@
┃|┃
┃|┃ ▓▓▒ ▓▓
┃|┃ ▓▓ ▓▓▒
┃|┃ ▓▓▓ ▓▓ ▓▓▓ ▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▬█▓▓▓▓▓▓▬█▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┃|┃ ▓▓▓▓░██░░▓▓░░██░▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
━━▓▓▓▓━━ ▓▓░░░░░░░░ ░░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▓ ▓░░░░░░░░░░░░░░░░░░░░▓▓▒▒▒▒▒▒▒▒▒▒▒▒
┃ ▓▓▓▓▓ ▓░░░░░░░░▄▄▄▄░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒
┃ ▓▓▓▓▓ ▓▓░░░░░░░░░░░░░░░▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓ ▓▓▓▓░░░░░░░▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▓▒▒░░░░░░░░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▒░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒
▓▒▒░░░░░░░░░░░░▓▓▓▓▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
▓▓▒░░░░░░░░░░░░░░░▓▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒
▓▓▒▒░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒
▓▓▓▒░░░░░░░░░░░░░▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▓▓▒▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒
▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▒▒▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒▒▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▒
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓ ░
▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓ ░░
▓▓▓▓▓▓▓▓▒▒░░░▒▒▒▒░░░░░░▓▓░▒▒▒▓▓▓▓▓▓▓▓▓▓░░░ ░
▓▓▓▓▓▓▓▒░░░░░░░░░▒░░░░░░░░░░░░▒▒▒▓▓▓▓▓▓▓▓░░ ░░▒
░ ░░░░░▒░░░░░░▒░░░▒░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░▒
▒▒░░▓▓░░▒░░░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░░░░░▒░░░░░▒ ░░
▒▒▒▒▒▓▒▒▓░░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░
▒▒█▒█▒▒▒▓░░▒░░░░░░░░░░░░░░░▒░░░░░░░░▒░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒█▒▒▒▒░░░░▒░░░▒░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░▒░░░
▓█▒▒▒▒█▒█▒▒▒▒░░▒░░░░░▒░░░░▒░░░░░░░░░░░░░░░░░▒░░░░▒░░░░░░░▒░░░░░▒▒
██▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒░░░░░░▒░░░░░░░░▒░░░░░░▒░░░░░░▒░░░░░▒░░░░░
▒▒▒▒█▒▒▒▒▒▒▒░░░░░░░░░░▒░░░░░░░░░░▒░░░░░░░░░░░▒░░░░░░░░░░░░░░░
▒▒█▒▒▒▒▒░▒░▒░░░░▓▓▓░░░░░░░▒░░░░▒░░░▒░░░░░░░▓▓░░░░░░░░░░░░ ░
▒▒▒▒▒▒▒░▒░░░▓▓▓▓▓▓░░░░░░░▒░░░░░░░░▒░░░░▓▓▓▓▓▓░░░░░░░░ ░
░▓▓▓▓▓▓░░░░░░▒░░░░░░░░▒░░░░░░▓▓▓▓▓░░░ ░ ░░

View File

@ -1,8 +1,8 @@
1 6
####### #############
#.....# #...........#
#.H...# #...........#
#.....# #####...........#
#.....# #...............#
#.....# #............H..#
#.##### #.###...........#
#.# #.# #...........#
#.# #.# #############

View File

@ -1,6 +1,6 @@
1 17
########### #########
#.........# #.......#
#....H....# #.......#
#.........# ############.......#
#.........###############..........#.......##############
#.........#........................#....................#
@ -13,7 +13,7 @@
########.##########......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# #.........#
#...........##......# #.........# ################.######
#...........##..H...# #.........# ################.######
#...........##......# #.........# #.................############
#...........##......# ########.########.......#.........#..........#
#...........##......# #...............#.......#.........#..........#

View File

@ -0,0 +1,41 @@
1 6
################################################################################
#..............................................................................#
#..#...........................................................................#
#...........#..................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
################################################################################

View File

@ -0,0 +1,97 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from ..display.display import Box, Display
from ..game import Game
from ..resources import ResourceManager
from ..translations import gettext as _
class CreditsDisplay(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.box = Box(*args, **kwargs)
self.pad = self.newpad(1, 1)
self.ascii_art_displayed = False
def update(self, game: Game) -> None:
return
def display(self) -> None:
self.box.refresh(self.y, self.x, self.height, self.width)
self.box.display()
self.pad.erase()
messages = [
_("Credits"),
"",
"Squirrel Battle",
"",
_("Developers:"),
"Yohann \"ÿnérant\" D'ANELLO",
"Mathilde \"eichhornchen\" DÉPRÉS",
"Nicolas \"nicomarg\" MARGULIES",
"Charles \"charsle\" PEYRAT",
"",
_("Translators:"),
"Hugo \"ifugao\" JACOB (español)",
]
for i, msg in enumerate(messages):
self.addstr(self.pad, i + (self.height - len(messages)) // 2,
(self.width - len(msg)) // 2, msg,
bold=(i == 0), italic=(":" in msg))
if self.ascii_art_displayed:
self.display_ascii_art()
self.refresh_pad(self.pad, 0, 0, self.y + 1, self.x + 1,
self.height + self.y - 2,
self.width + self.x - 2)
def display_ascii_art(self) -> None:
with open(ResourceManager.get_asset_path("ascii-art-ecureuil.txt"))\
as f:
ascii_art = f.read().split("\n")
height, width = len(ascii_art), len(ascii_art[0])
y_offset, x_offset = (self.height - height) // 2,\
(self.width - width) // 2
for i, line in enumerate(ascii_art):
for j, c in enumerate(line):
bg_color = curses.COLOR_WHITE
fg_color = curses.COLOR_BLACK
bold = False
if c == ' ':
bg_color = curses.COLOR_BLACK
elif c == '' or c == '' or c == '':
bold = True
fg_color = curses.COLOR_WHITE
bg_color = curses.COLOR_BLACK
elif c == '|':
bold = True # c = '┃'
fg_color = (100, 700, 1000)
bg_color = curses.COLOR_BLACK
elif c == '':
fg_color = (700, 300, 0)
elif c == '':
fg_color = (700, 300, 0)
bg_color = curses.COLOR_BLACK
elif c == '':
fg_color = (350, 150, 0)
elif c == '':
fg_color = (0, 0, 0)
bg_color = curses.COLOR_BLACK
elif c == '':
c = ''
fg_color = (1000, 1000, 1000)
bg_color = curses.COLOR_BLACK
self.addstr(self.pad, y_offset + i, x_offset + j, c,
fg_color, bg_color, bold=bold)
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
if self.pad.inch(y - 1, x - 1) != ord(" "):
self.ascii_art_displayed = True

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import Any, Optional, Union
import sys
from typing import Any, Optional, Tuple, Union
from squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.game import Game
@ -16,14 +17,24 @@ class Display:
height: int
pad: Any
_color_pairs = {(curses.COLOR_WHITE, curses.COLOR_BLACK): 0}
_colors_rgb = {}
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
self.screen = screen
self.pack = pack or TexturePack.get_pack("ascii")
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
"""
Overwrites the native curses function of the same name.
"""
return curses.newpad(height, width) if self.screen else FakePad()
def truncate(self, msg: str, height: int, width: int) -> str:
"""
Truncates a string into a string adapted to the width and height of
the screen.
"""
height = max(0, height)
width = max(0, width)
lines = msg.split("\n")
@ -31,15 +42,86 @@ class Display:
lines = [line[:width] for line in lines]
return "\n".join(lines)
def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None:
def translate_color(self, color: Union[int, Tuple[int, int, int]]) -> int:
"""
Display a message onto the pad.
Translates a tuple (R, G, B) into a curses color index.
If we already have a color index, then nothing is processed.
If this is a tuple, we construct a new color index if non-existing
and we return this index.
The values of R, G and B must be between 0 and 1000, and not
between 0 and 255.
"""
if isinstance(color, tuple):
# The color is a tuple (R, G, B), that is potentially unknown.
# We translate it into a curses color number.
if color not in self._colors_rgb:
# The color does not exist, we create it.
color_nb = len(self._colors_rgb) + 8
self.init_color(color_nb, color[0], color[1], color[2])
self._colors_rgb[color] = color_nb
color = self._colors_rgb[color]
return color
def addstr(self, pad: Any, y: int, x: int, msg: str,
fg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_WHITE,
bg_color: Union[int, Tuple[int, int, int]] = curses.COLOR_BLACK,
*, altcharset: bool = False, blink: bool = False,
bold: bool = False, dim: bool = False, invis: bool = False,
italic: bool = False, normal: bool = False,
protect: bool = False, reverse: bool = False,
standout: bool = False, underline: bool = False,
horizontal: bool = False, left: bool = False,
low: bool = False, right: bool = False, top: bool = False,
vertical: bool = False, chartext: bool = False) -> None:
"""
Displays a message onto the pad.
If the message is too large, it is truncated vertically and horizontally
The text can be bold, italic, blinking, ... if the right parameters are
given. These parameters are translated into curses attributes.
The foreground and background colors can be given as curses constants
(curses.COLOR_*), or by giving a tuple (R, G, B) that corresponds to
the color. R, G, B must be between 0 and 1000, and not 0 and 255.
"""
height, width = pad.getmaxyx()
# Truncate message if it is too large
msg = self.truncate(msg, height - y, width - x - 1)
if msg.replace("\n", "") and x >= 0 and y >= 0:
return pad.addstr(y, x, msg, *options)
fg_color = self.translate_color(fg_color)
bg_color = self.translate_color(bg_color)
# Get the pair number for the tuple (fg, bg)
# If it does not exist, create it and give a new unique id.
if (fg_color, bg_color) in self._color_pairs:
pair_nb = self._color_pairs[(fg_color, bg_color)]
else:
pair_nb = len(self._color_pairs)
self.init_pair(pair_nb, fg_color, bg_color)
self._color_pairs[(fg_color, bg_color)] = pair_nb
# Compute curses attributes from the parameters
attr = self.color_pair(pair_nb)
attr |= curses.A_ALTCHARSET if altcharset else 0
attr |= curses.A_BLINK if blink else 0
attr |= curses.A_BOLD if bold else 0
attr |= curses.A_DIM if dim else 0
attr |= curses.A_INVIS if invis else 0
# Italic is supported since Python 3.7
italic &= sys.version_info >= (3, 7,)
attr |= curses.A_ITALIC if italic else 0
attr |= curses.A_NORMAL if normal else 0
attr |= curses.A_PROTECT if protect else 0
attr |= curses.A_REVERSE if reverse else 0
attr |= curses.A_STANDOUT if standout else 0
attr |= curses.A_UNDERLINE if underline else 0
attr |= curses.A_HORIZONTAL if horizontal else 0
attr |= curses.A_LEFT if left else 0
attr |= curses.A_LOW if low else 0
attr |= curses.A_RIGHT if right else 0
attr |= curses.A_TOP if top else 0
attr |= curses.A_VERTICAL if vertical else 0
attr |= curses.A_CHARTEXT if chartext else 0
return pad.addstr(y, x, msg, attr)
def init_pair(self, number: int, foreground: int, background: int) -> None:
return curses.init_pair(number, foreground, background) \
@ -48,17 +130,28 @@ class Display:
def color_pair(self, number: int) -> int:
return curses.color_pair(number) if self.screen else 0
def init_color(self, number: int, red: int, green: int, blue: int) -> None:
return curses.init_color(number, red, green, blue) \
if self.screen else None
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
"""
Resizes a pad.
"""
self.x = x
self.y = y
self.width = width
self.height = height
if hasattr(self, "pad") and resize_pad and \
self.height >= 0 and self.width >= 0:
self.pad.erase()
self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
"""
Refreshes a pad
"""
if len(args) == 4:
self.resize(*args, resize_pad)
self.display()
@ -67,10 +160,10 @@ class Display:
window_y: int, window_x: int,
last_y: int, last_x: int) -> None:
"""
Refresh a pad on a part of the window.
Refreshes a pad on a part of the window.
The refresh starts at coordinates (top_y, top_x) from the pad,
and is drawn from (window_y, window_x) to (last_y, last_x).
If coordinates are invalid (negative indexes/length..., then nothing
If coordinates are invalid (negative indexes/length...), then nothing
is drawn and no error is raised.
"""
top_y, top_x = max(0, top_y), max(0, top_x)
@ -82,17 +175,26 @@ class Display:
if last_y >= window_y and last_x >= window_x:
# Refresh the pad only if coordinates are valid
pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x)
pad.noutrefresh(top_y, top_x, window_y, window_x, last_y, last_x)
def display(self) -> None:
"""
Draw the content of the display and refresh pads.
"""
raise NotImplementedError
def handle_click(self, y: int, x: int, game: Game) -> None:
def update(self, game: Game) -> None:
"""
The game state was updated.
Indicate what to do with the new state.
"""
raise NotImplementedError
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
"""
A mouse click was performed on the coordinates (y, x) of the pad.
Maybe it can do something.
Maybe it should do something.
"""
pass
@property
def rows(self) -> int:
@ -104,7 +206,9 @@ class Display:
class VerticalSplit(Display):
"""
A class to split the screen in two vertically with a pretty line.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, 1)
@ -125,7 +229,9 @@ class VerticalSplit(Display):
class HorizontalSplit(Display):
"""
A class to split the screen in two horizontally with a pretty line.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(1, self.cols)
@ -146,23 +252,32 @@ class HorizontalSplit(Display):
class Box(Display):
"""
A class for pretty boxes to print menus and other content.
"""
title: str = ""
def update_title(self, title: str) -> None:
self.title = title
def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.fg_border_color = fg_border_color or curses.COLOR_WHITE
pair_number = 4 + self.fg_border_color
self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK)
self.pair = self.color_pair(pair_number)
def display(self) -> None:
self.addstr(self.pad, 0, 0, "" + "" * (self.width - 2) + "",
self.pair)
self.fg_border_color)
for i in range(1, self.height - 1):
self.addstr(self.pad, i, 0, "", self.pair)
self.addstr(self.pad, i, self.width - 1, "", self.pair)
self.addstr(self.pad, i, 0, "", self.fg_border_color)
self.addstr(self.pad, i, self.width - 1, "", self.fg_border_color)
self.addstr(self.pad, self.height - 1, 0,
"" + "" * (self.width - 2) + "", self.pair)
"" + "" * (self.width - 2) + "", self.fg_border_color)
if self.title:
self.addstr(self.pad, 0, (self.width - len(self.title) - 8) // 2,
f" == {self.title} == ", curses.COLOR_GREEN,
italic=True, bold=True)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1)

View File

@ -2,6 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.creditsdisplay import CreditsDisplay
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit, \
Display
from squirrelbattle.display.mapdisplay import MapDisplay
@ -30,17 +32,21 @@ class DisplayManager:
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.messagedisplay = MessageDisplay(screen=screen, pack=None)
self.messagedisplay = MessageDisplay(screen, pack)
self.hbar = HorizontalSplit(screen, pack)
self.vbar = VerticalSplit(screen, pack)
self.creditsdisplay = CreditsDisplay(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay, self.messagedisplay,
self.playerinventorydisplay,
self.storeinventorydisplay]
self.storeinventorydisplay, self.creditsdisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions, *params) -> None:
"""
Handles the differents values of display action.
"""
if action == DisplayActions.REFRESH:
self.refresh()
elif action == DisplayActions.UPDATE:
@ -49,19 +55,18 @@ class DisplayManager:
self.handle_mouse_click(*params)
def update_game_components(self) -> None:
"""
The game state was updated.
Trigger all displays of these modifications.
"""
for d in self.displays:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player)
self.game.inventory_menu.update_player(self.game.player)
self.game.store_menu.update_merchant(self.game.player)
self.playerinventorydisplay.update_menu(self.game.inventory_menu)
self.storeinventorydisplay.update_menu(self.game.store_menu)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message)
d.update(self.game)
def handle_mouse_click(self, y: int, x: int) -> None:
def handle_mouse_click(self, y: int, x: int, attr: int) -> None:
"""
Handles the mouse clicks.
"""
displays = self.refresh()
display = None
for d in displays:
@ -71,10 +76,14 @@ class DisplayManager:
# of that display
display = d
if display:
display.handle_click(y - display.y, x - display.x, self.game)
display.handle_click(y - display.y, x - display.x, attr, self.game)
def refresh(self) -> List[Display]:
"""
Refreshes all components on the screen.
"""
displays = []
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
if self.game.state == GameMode.PLAY \
or self.game.state == GameMode.INVENTORY \
@ -97,27 +106,41 @@ class DisplayManager:
if self.game.state == GameMode.INVENTORY:
self.playerinventorydisplay.refresh(
self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5)
self.rows // 10,
pack.tile_width * (self.cols // (2 * pack.tile_width)),
8 * self.rows // 10,
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
displays.append(self.playerinventorydisplay)
elif self.game.state == GameMode.STORE:
self.storeinventorydisplay.refresh(
self.rows // 10, self.cols // 2,
8 * self.rows // 10, 2 * self.cols // 5)
self.rows // 10,
pack.tile_width * (self.cols // (2 * pack.tile_width)),
8 * self.rows // 10,
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
self.playerinventorydisplay.refresh(
self.rows // 10,
pack.tile_width * (self.cols // (10 * pack.tile_width)),
8 * self.rows // 10,
pack.tile_width * (2 * self.cols // (5 * pack.tile_width)))
displays.append(self.storeinventorydisplay)
displays.append(self.playerinventorydisplay)
elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.mainmenudisplay)
elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.settingsmenudisplay)
elif self.game.state == GameMode.CREDITS:
self.creditsdisplay.refresh(0, 0, self.rows, self.cols)
displays.append(self.creditsdisplay)
if self.game.message:
height, width = 0, 0
for line in self.game.message.split("\n"):
height += 1
width = max(width, len(line))
y, x = (self.rows - height) // 2, (self.cols - width) // 2
y = pack.tile_width * (self.rows - height) // (2 * pack.tile_width)
x = pack.tile_width * ((self.cols - width) // (2 * pack.tile_width))
self.messagedisplay.refresh(y, x, height, width)
displays.append(self.messagedisplay)
@ -127,7 +150,7 @@ class DisplayManager:
def resize_window(self) -> bool:
"""
If the window got resized, ensure that the screen size got updated.
When the window is resized, ensures that the screen size is updated.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows,
@ -138,8 +161,16 @@ class DisplayManager:
@property
def rows(self) -> int:
"""
Overwrites the native curses attribute of the same name,
for testing purposes.
"""
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
"""
Overwrites the native curses attribute of the same name,
for testing purposes.
"""
return curses.COLS if self.screen else 42

View File

@ -2,17 +2,23 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.display.display import Display
from squirrelbattle.game import Game
from squirrelbattle.interfaces import Logs
class LogsDisplay(Display):
"""
A class to handle the display of the logs.
"""
logs: Logs
def __init__(self, *args) -> None:
super().__init__(*args)
self.pad = self.newpad(self.rows, self.cols)
def update_logs(self, logs: Logs) -> None:
self.logs = logs
def update(self, game: Game) -> None:
self.logs = game.logs
def display(self) -> None:
messages = self.logs.messages[-self.height:]

View File

@ -3,27 +3,42 @@
from squirrelbattle.interfaces import Map
from .display import Display
from ..game import Game
class MapDisplay(Display):
"""
A class to handle the display of the map.
"""
map: Map
def __init__(self, *args):
super().__init__(*args)
def update_map(self, m: Map) -> None:
self.map = m
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update(self, game: Game) -> None:
self.map = game.map
self.pad = self.newpad(self.map.height,
self.pack.tile_width * self.map.width + 1)
def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for j in range(len(self.map.tiles)):
for i in range(len(self.map.tiles[j])):
if not self.map.seen_tiles[j][i]:
continue
fg, bg = self.map.tiles[j][i].visible_color(self.pack) if \
self.map.visibility[j][i] else \
self.map.tiles[j][i].hidden_color(self.pack)
self.addstr(self.pad, j, self.pack.tile_width * i,
self.map.tiles[j][i].char(self.pack), fg, bg)
for e in self.map.entities:
self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
if self.map.visibility[e.y][e.x]:
self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()],
self.pack.entity_fg_color,
self.pack.entity_bg_color)
# Display Path map for deubg purposes
# Display Path map for debug purposes
# from squirrelbattle.entities.player import Player
# players = [ p for p in self.map.entities if isinstance(p,Player) ]
# player = players[0] if len(players) > 0 else None
@ -42,7 +57,8 @@ class MapDisplay(Display):
# else:
# character = '←'
# self.addstr(self.pad, y, self.pack.tile_width * x,
# character, self.color_pair(1))
# character, self.pack.tile_fg_color,
# self.pack.tile_bg_color)
def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx

View File

@ -2,11 +2,13 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from random import randint
from typing import List
from squirrelbattle.menus import Menu, MainMenu
from .display import Display, Box
from ..enums import KeyValues
from squirrelbattle.menus import Menu, MainMenu, SettingsMenu, StoreMenu
from .display import Box, Display
from ..entities.player import Player
from ..enums import KeyValues, GameMode
from ..game import Game
from ..resources import ResourceManager
from ..translations import gettext as _
@ -14,8 +16,9 @@ from ..translations import gettext as _
class MenuDisplay(Display):
"""
A class to display the menu objects
A class to display the menu objects.
"""
menu: Menu
position: int
def __init__(self, *args, **kwargs):
@ -30,9 +33,9 @@ class MenuDisplay(Display):
def update_pad(self) -> None:
for i in range(self.trueheight):
self.addstr(self.pad, i, 0, " " + self.values[i])
self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line
self.addstr(self.pad, self.menu.position, 0, ">")
self.addstr(self.pad, self.menu.position, 0, " >")
def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
@ -43,11 +46,11 @@ class MenuDisplay(Display):
self.menubox.refresh(self.y, self.x, self.height, self.width)
self.pad.erase()
self.update_pad()
self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2,
self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 1,
self.height - 2 + self.y,
self.width - 2 + self.x)
def handle_click(self, y: int, x: int, game: Game) -> None:
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
"""
We can select a menu item with the mouse.
"""
@ -77,8 +80,13 @@ class MenuDisplay(Display):
class SettingsMenuDisplay(MenuDisplay):
"""
A class to display specifically a settingsmenu object
A class to display specifically a settingsmenu object.
"""
menu: SettingsMenu
def update(self, game: Game) -> None:
self.update_menu(game.settings_menu)
@property
def values(self) -> List[str]:
return [_(a[1][1]) + (" : "
@ -90,7 +98,7 @@ class SettingsMenuDisplay(MenuDisplay):
class MainMenuDisplay(Display):
"""
A class to display specifically a mainmenu object
A class to display specifically a mainmenu object.
"""
def __init__(self, menu: MainMenu, *args):
super().__init__(*args)
@ -102,13 +110,18 @@ class MainMenuDisplay(Display):
self.pad = self.newpad(max(self.rows, len(self.title) + 30),
max(len(self.title[0]) + 5, self.cols))
self.fg_color = curses.COLOR_WHITE
self.menudisplay = MenuDisplay(self.screen, self.pack)
self.menudisplay.update_menu(self.menu)
def display(self) -> None:
for i in range(len(self.title)):
self.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i])
- len(self.title[0]) // 2 - 1, 0), self.title[i],
self.fg_color)
msg = _("Credits")
self.addstr(self.pad, self.height - 1, self.width - 1 - len(msg), msg)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)
@ -118,26 +131,56 @@ class MainMenuDisplay(Display):
menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth)
def handle_click(self, y: int, x: int, game: Game) -> None:
def update(self, game: Game) -> None:
self.menudisplay.update_menu(game.main_menu)
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
menuheight = min(self.menudisplay.preferred_height, self.height - menuy)
if menuy <= y < menuy + menuheight and menux <= x < menux + menuwidth:
self.menudisplay.handle_click(y - menuy, x - menux, game)
self.menudisplay.handle_click(y - menuy, x - menux, attr, game)
if y <= len(self.title):
self.fg_color = randint(0, 1000), randint(0, 1000), randint(0, 1000)
if y == self.height - 1 and x >= self.width - 1 - len(_("Credits")):
game.state = GameMode.CREDITS
class PlayerInventoryDisplay(MenuDisplay):
message = _("== INVENTORY ==")
"""
A class to handle the display of the player's inventory.
"""
player: Player = None
selected: bool = True
store_mode: bool = False
def update(self, game: Game) -> None:
self.player = game.player
self.update_menu(game.inventory_menu)
self.store_mode = game.state == GameMode.STORE
self.selected = game.state == GameMode.INVENTORY \
or (self.store_mode and not game.is_in_store_menu)
def update_pad(self) -> None:
self.addstr(self.pad, 0, (self.width - len(self.message)) // 2,
self.message, curses.A_BOLD | curses.A_ITALIC)
self.menubox.update_title(_("INVENTORY"))
for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()]
selection = f"[{rep}]" if i == self.menu.position else f" {rep} "
self.addstr(self.pad, 2 + i, 0, selection
+ " " + item.translated_name.capitalize())
selection = f"[{rep}]" if i == self.menu.position \
and self.selected else f" {rep} "
self.addstr(self.pad, i + 1, 0, selection
+ " " + item.translated_name.capitalize()
+ (f" ({item.description})" if item.description else "")
+ (": " + str(item.price) + " Hazels"
if self.store_mode else ""))
if self.store_mode:
price = f"{self.pack.HAZELNUT} {self.player.hazel} Hazels"
width = len(price) + (self.pack.tile_width - 1)
self.addstr(self.pad, self.height - 3, self.width - width - 2,
price, italic=True)
@property
def truewidth(self) -> int:
@ -147,27 +190,42 @@ class PlayerInventoryDisplay(MenuDisplay):
def trueheight(self) -> int:
return 2 + super().trueheight
def handle_click(self, y: int, x: int, game: Game) -> None:
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
"""
We can select a menu item with the mouse.
"""
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 3))
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2))
game.is_in_store_menu = False
game.handle_key_pressed(KeyValues.ENTER)
class StoreInventoryDisplay(MenuDisplay):
message = _("== STALL ==")
"""
A class to handle the display of a merchant's inventory.
"""
menu: StoreMenu
selected: bool = False
def update(self, game: Game) -> None:
self.update_menu(game.store_menu)
self.selected = game.is_in_store_menu
def update_pad(self) -> None:
self.addstr(self.pad, 0, (self.width - len(self.message)) // 2,
self.message, curses.A_BOLD | curses.A_ITALIC)
self.menubox.update_title(_("STALL"))
for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()]
selection = f"[{rep}]" if i == self.menu.position else f" {rep} "
self.addstr(self.pad, 2 + i, 0, selection
selection = f"[{rep}]" if i == self.menu.position \
and self.selected else f" {rep} "
self.addstr(self.pad, i + 1, 0, selection
+ " " + item.translated_name.capitalize()
+ (f" ({item.description})" if item.description else "")
+ ": " + str(item.price) + " Hazels")
price = f"{self.pack.HAZELNUT} {self.menu.merchant.hazel} Hazels"
width = len(price) + (self.pack.tile_width - 1)
self.addstr(self.pad, self.height - 3, self.width - width - 2, price,
italic=True)
@property
def truewidth(self) -> int:
return max(1, self.height if hasattr(self, "height") else 10)
@ -176,9 +234,10 @@ class StoreInventoryDisplay(MenuDisplay):
def trueheight(self) -> int:
return 2 + super().trueheight
def handle_click(self, y: int, x: int, game: Game) -> None:
def handle_click(self, y: int, x: int, attr: int, game: Game) -> None:
"""
We can select a menu item with the mouse.
"""
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 3))
self.menu.position = max(0, min(len(self.menu.values) - 1, y - 2))
game.is_in_store_menu = True
game.handle_key_pressed(KeyValues.ENTER)

View File

@ -3,11 +3,12 @@
import curses
from squirrelbattle.display.display import Box, Display
from squirrelbattle.game import Game
class MessageDisplay(Display):
"""
Display a message in a popup.
A class to handle the display of popup messages.
"""
def __init__(self, *args, **kwargs):
@ -17,15 +18,15 @@ class MessageDisplay(Display):
self.message = ""
self.pad = self.newpad(1, 1)
def update_message(self, msg: str) -> None:
self.message = msg
def update(self, game: Game) -> None:
self.message = game.message
def display(self) -> None:
self.box.refresh(self.y - 1, self.x - 2,
self.height + 2, self.width + 4)
self.box.display()
self.pad.erase()
self.addstr(self.pad, 0, 0, self.message, curses.A_BOLD)
self.addstr(self.pad, 0, 0, self.message, bold=True)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)

View File

@ -3,32 +3,42 @@
import curses
from ..entities.items import Monocle
from ..entities.player import Player
from ..game import Game
from ..interfaces import FightingEntity
from ..translations import gettext as _
from .display import Display
class StatsDisplay(Display):
"""
A class to handle the display of the stats of the player.
"""
game: Game
player: Player
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
def update_player(self, p: Player) -> None:
self.player = p
def update(self, game: Game) -> None:
self.game = game
self.player = game.player
def update_pad(self) -> None:
string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
string2 = f"{_(self.player.name).capitalize()} " \
f"-- LVL {self.player.level} -- " \
f"FLOOR {-self.player.map.floor}\n" \
f"EXP {self.player.current_xp}/{self.player.max_xp}\n" \
f"HP {self.player.health}/{self.player.maxhealth}"
self.addstr(self.pad, 0, 0, string2)
string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
string3 = f"STR {self.player.strength}\n" \
f"INT {self.player.intelligence}\n" \
f"CHR {self.player.charisma}\n" \
f"DEX {self.player.dexterity}\n" \
f"CON {self.player.constitution}\n" \
f"CRI {self.player.critical}%"
self.addstr(self.pad, 3, 0, string3)
inventory_str = _("Inventory:") + " "
@ -44,15 +54,73 @@ class StatsDisplay(Display):
if count > 1:
inventory_str += f"x{count} "
printed_items.append(item)
self.addstr(self.pad, 8, 0, inventory_str)
self.addstr(self.pad, 9, 0, inventory_str)
self.addstr(self.pad, 9, 0, f"{self.pack.HAZELNUT} "
f"x{self.player.hazel}")
if self.player.equipped_main:
self.addstr(self.pad, 10, 0,
_("Equipped main:") + " "
f"{self.pack[self.player.equipped_main.name.upper()]}")
if self.player.equipped_secondary:
self.addstr(self.pad, 11, 0,
_("Equipped secondary:") + " "
+ self.pack[self.player.equipped_secondary
.name.upper()])
if self.player.equipped_armor:
self.addstr(self.pad, 12, 0,
_("Equipped chestplate:") + " "
+ self.pack[self.player.equipped_armor.name.upper()])
if self.player.equipped_helmet:
self.addstr(self.pad, 13, 0,
_("Equipped helmet:") + " "
+ self.pack[self.player.equipped_helmet.name.upper()])
self.addstr(self.pad, 14, 0, f"{self.pack.HAZELNUT} "
f"x{self.player.hazel}")
if self.player.dead:
self.addstr(self.pad, 11, 0, _("YOU ARE DEAD"),
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
self.addstr(self.pad, 15, 0, _("YOU ARE DEAD"), curses.COLOR_RED,
bold=True, blink=True, standout=True)
if self.player.map.tiles[self.player.y][self.player.x].is_ladder():
msg = _("Use {key} to use the ladder") \
.format(key=self.game.settings.KEY_LADDER.upper())
self.addstr(self.pad, self.height - 2, 0, msg,
italic=True, reverse=True)
self.update_entities_stats()
def update_entities_stats(self) -> None:
"""
Display information about a near entity if we have a monocle.
"""
for dy, dx in [(-1, 0), (0, -1), (0, 1), (1, 0)]:
for entity in self.player.map.find_entities(FightingEntity):
if entity == self.player:
continue
if entity.y == self.player.y + dy \
and entity.x == self.player.x + dx:
if entity.is_friendly():
msg = _("Move to the friendly entity to talk to it") \
if self.game.waiting_for_friendly_key else \
_("Use {key} then move to talk to the entity") \
.format(key=self.game.settings.KEY_CHAT.upper())
self.addstr(self.pad, self.height - 1, 0, msg,
italic=True, reverse=True)
if isinstance(self.player.equipped_secondary, Monocle):
# Truth monocle
message = f"{entity.translated_name.capitalize()} " \
f"{self.pack[entity.name.upper()]}\n" \
f"STR {entity.strength}\n" \
f"INT {entity.intelligence}\n" \
f"CHR {entity.charisma}\n" \
f"DEX {entity.dexterity}\n" \
f"CON {entity.constitution}\n" \
f"CRI {entity.critical}%"
self.addstr(self.pad, 17, 0, message)
# Only display one entity
break
def display(self) -> None:
self.pad.erase()

View File

@ -2,33 +2,44 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import Any
from typing import Any, Union, Tuple
class TexturePack:
"""
A class to handle displaying several textures.
"""
_packs = dict()
name: str
tile_width: int
tile_fg_color: int
tile_bg_color: int
entity_fg_color: int
entity_bg_color: int
tile_fg_color: Union[int, Tuple[int, int, int]]
tile_fg_visible_color: Union[int, Tuple[int, int, int]]
tile_bg_color: Union[int, Tuple[int, int, int]]
entity_fg_color: Union[int, Tuple[int, int, int]]
entity_bg_color: Union[int, Tuple[int, int, int]]
BODY_SNATCH_POTION: str
BOMB: str
HEART: str
HEDGEHOG: str
CHESTPLATE: str
EAGLE: str
EMPTY: str
FLOOR: str
HAZELNUT: str
HEART: str
HEDGEHOG: str
HELMET: str
MERCHANT: str
PLAYER: str
RABBIT: str
RING_OF_CRITICAL_DAMAGE: str
RING_OF_MORE_EXPERIENCE: str
SHIELD: str
SUNFLOWER: str
SWORD: str
TEDDY_BEAR: str
TIGER: str
TRUMPET: str
WALL: str
ASCII_PACK: "TexturePack"
@ -54,49 +65,72 @@ class TexturePack:
TexturePack.ASCII_PACK = TexturePack(
name="ascii",
tile_width=1,
tile_fg_visible_color=(1000, 1000, 1000),
tile_fg_color=curses.COLOR_WHITE,
tile_bg_color=curses.COLOR_BLACK,
entity_fg_color=curses.COLOR_WHITE,
entity_fg_color=(1000, 1000, 1000),
entity_bg_color=curses.COLOR_BLACK,
BODY_SNATCH_POTION='S',
BOMB='o',
BOMB='ç',
CHESTPLATE='(',
EAGLE='µ',
EMPTY=' ',
EXPLOSION='%',
FLOOR='.',
LADDER='H',
HAZELNUT='¤',
HEART='',
HEDGEHOG='*',
HELMET='0',
MERCHANT='M',
MONOCLE='ô',
PLAYER='@',
RABBIT='Y',
RING_OF_CRITICAL_DAMAGE='o',
RING_OF_MORE_EXPERIENCE='o',
SHIELD='D',
SUNFLOWER='I',
SWORD='\u2020',
TEDDY_BEAR='8',
TIGER='n',
TRUMPET='/',
WALL='#',
)
TexturePack.SQUIRREL_PACK = TexturePack(
name="squirrel",
tile_width=2,
tile_fg_visible_color=(1000, 1000, 1000),
tile_fg_color=curses.COLOR_WHITE,
tile_bg_color=curses.COLOR_BLACK,
entity_fg_color=curses.COLOR_WHITE,
entity_bg_color=curses.COLOR_WHITE,
entity_fg_color=(1000, 1000, 1000),
entity_bg_color=(1000, 1000, 1000),
BODY_SNATCH_POTION='🔀',
BOMB='💣',
CHESTPLATE='🦺',
EAGLE='🦅',
EMPTY=' ',
EXPLOSION='💥',
FLOOR='██',
LADDER=('🪜', curses.COLOR_WHITE, (1000, 1000, 1000),
curses.COLOR_WHITE, (1000, 1000, 1000)),
HAZELNUT='🌰',
HEART='💜',
HEDGEHOG='🦔',
HELMET='⛑️ ',
PLAYER='🐿️ ',
MERCHANT='🦜',
MONOCLE='🧐',
RABBIT='🐇',
RING_OF_CRITICAL_DAMAGE='💍',
RING_OF_MORE_EXPERIENCE='💍',
SHIELD='🛡️ ',
SUNFLOWER='🌻',
SWORD='🗡️',
SWORD='🗡️ ',
TEDDY_BEAR='🧸',
TIGER='🐅',
TRUMPET='🎺',
WALL='🧱',
)

View File

@ -1,17 +1,18 @@
from ..interfaces import FriendlyEntity, InventoryHolder
from ..interfaces import FriendlyEntity, InventoryHolder, Map, FightingEntity
from ..translations import gettext as _
from .player import Player
from .monsters import Monster
from .items import Item
from random import choice
from random import choice, shuffle
class Merchant(InventoryHolder, FriendlyEntity):
"""
The class for merchants in the dungeon
The class of merchants in the dungeon.
"""
def keys(self) -> list:
"""
Returns a friendly entitie's specific attributes
Returns a friendly entitie's specific attributes.
"""
return super().keys() + ["inventory", "hazel"]
@ -20,7 +21,6 @@ class Merchant(InventoryHolder, FriendlyEntity):
super().__init__(name=name, *args, **kwargs)
self.inventory = self.translate_inventory(inventory or [])
self.hazel = hazel
if not self.inventory:
for i in range(5):
self.inventory.append(choice(Item.get_all_items())())
@ -28,23 +28,101 @@ class Merchant(InventoryHolder, FriendlyEntity):
def talk_to(self, player: Player) -> str:
"""
This function is used to open the merchant's inventory in a menu,
and allow the player to buy/sell objects
and allows the player to buy/sell objects.
"""
return _("I don't sell any squirrel")
def change_hazel_balance(self, hz: int) -> None:
"""
Change the number of hazel the merchant has by hz.
Changes the number of hazel the merchant has by hz.
"""
self.hazel += hz
class Sunflower(FriendlyEntity):
"""
A friendly sunflower
A friendly sunflower.
"""
dialogue_option = [_("Flower power!!"), _("The sun is warm today")]
def __init__(self, maxhealth: int = 15,
*args, **kwargs) -> None:
super().__init__(name="sunflower", maxhealth=maxhealth, *args, **kwargs)
@property
def dialogue_option(self) -> list:
"""
Lists all that a sunflower can say to the player.
"""
return [_("Flower power!!"), _("The sun is warm today")]
class Familiar(FightingEntity):
"""
A friendly familiar that helps the player defeat monsters.
"""
def __init__(self, maxhealth: int = 25,
*args, **kwargs) -> None:
super().__init__(maxhealth=maxhealth, *args, **kwargs)
self.target = None
# @property
# def dialogue_option(self) -> list:
# """
# Debug function (to see if used in the real game)
# """
# return [_("My target is"+str(self.target))]
def act(self, p: Player, m: Map) -> None:
"""
By default, the familiar tries to stay at distance at most 2 of the
player and if a monster comes in range 3, it focuses on the monster
and attacks it.
"""
if self.target is None:
# If the previous target is dead(or if there was no previous target)
# the familiar tries to get closer to the player.
self.target = p
elif self.target.dead:
self.target = p
if self.target == p:
# Look for monsters around the player to kill TOFIX : if monster is
# out of range, continue targetting player.
for entity in m.entities:
if (p.y - entity.y) ** 2 + (p.x - entity.x) ** 2 <= 9 and\
isinstance(entity, Monster):
self.target = entity
entity.paths = dict() # Allows the paths to be calculated.
break
# Familiars move according to a Dijkstra algorithm
# that targets their target.
# If they can not move and are already close to their target,
# they hit, except if their target is the player.
if self.target and (self.y, self.x) in self.target.paths:
# Moves to target player by choosing the best available path
for next_y, next_x in self.target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
break
if self.distance_squared(self.target) <= 1 and \
not isinstance(self.target, Player):
self.map.logs.add_message(self.hit(self.target))
break
else:
# Moves in a random direction
# If the direction is not available, tries another one
moves = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
for move in moves:
if move():
break
class Trumpet(Familiar):
"""
A class of familiars.
"""
def __init__(self, name: str = "trumpet", strength: int = 3,
maxhealth: int = 20, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -4,14 +4,13 @@
from random import choice, randint
from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map, InventoryHolder
from ..translations import gettext as _
class Item(Entity):
"""
A class for items
A class for items.
"""
held: bool
held_by: Optional[InventoryHolder]
@ -25,12 +24,19 @@ class Item(Entity):
self.held_by = held_by
self.price = price
@property
def description(self) -> str:
"""
In the inventory, indicate the usefulness of the item.
"""
return ""
def drop(self) -> None:
"""
The item is dropped from the inventory onto the floor
The item is dropped from the inventory onto the floor.
"""
if self.held:
self.held_by.inventory.remove(self)
self.held_by.remove_from_inventory(self)
self.held_by.map.add_entity(self)
self.move(self.held_by.y, self.held_by.x)
self.held = False
@ -45,19 +51,31 @@ class Item(Entity):
"""
Indicates what should be done when the item is equipped.
"""
# Other objects are only equipped as secondary.
if self.held_by.equipped_secondary:
self.held_by.equipped_secondary.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_secondary = self
def hold(self, player: InventoryHolder) -> None:
def unequip(self) -> None:
"""
The item is taken from the floor and put into the inventory
Indicates what should be done when the item is unequipped.
"""
self.held_by.remove_from_inventory(self)
self.held_by.add_to_inventory(self)
def hold(self, holder: InventoryHolder) -> None:
"""
The item is taken from the floor and put into the inventory.
"""
self.held = True
self.held_by = player
self.held_by = holder
self.held_by.map.remove_entity(self)
player.add_to_inventory(self)
holder.add_to_inventory(self)
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
Saves the state of the item into a dictionary.
"""
d = super().save_state()
d["held"] = self.held
@ -65,13 +83,17 @@ class Item(Entity):
@staticmethod
def get_all_items() -> list:
return [BodySnatchPotion, Bomb, Heart, Sword]
"""
Returns the list of all item classes.
"""
return [BodySnatchPotion, Chestplate, Bomb, Heart, Helmet, Monocle,
Shield, Sword, RingCritical, RingXP]
def be_sold(self, buyer: InventoryHolder, seller: InventoryHolder) -> bool:
"""
Does all necessary actions when an object is to be sold.
Is overwritten by some classes that cannot exist in the player's
inventory
inventory.
"""
if buyer.hazel >= self.price:
self.hold(buyer)
@ -85,7 +107,7 @@ class Item(Entity):
class Heart(Item):
"""
A heart item to return health to the player
A heart item to return health to the player.
"""
healing: int
@ -94,16 +116,21 @@ class Heart(Item):
super().__init__(name=name, price=price, *args, **kwargs)
self.healing = healing
@property
def description(self) -> str:
return f"HP+{self.healing}"
def hold(self, entity: InventoryHolder) -> None:
"""
When holding a heart, heal the player and don't put item in inventory.
When holding a heart, the player is healed and
the item is not put in the inventory.
"""
entity.health = min(entity.maxhealth, entity.health + self.healing)
entity.map.remove_entity(self)
def save_state(self) -> dict:
"""
Saves the state of the header into a dictionary
Saves the state of the heart into a dictionary.
"""
d = super().save_state()
d["healing"] = self.healing
@ -116,7 +143,7 @@ class Bomb(Item):
"""
damage: int = 5
exploding: bool
owner: Optional["Player"]
owner: Optional["InventoryHolder"]
tick: int
def __init__(self, name: str = "bomb", damage: int = 5,
@ -129,7 +156,7 @@ class Bomb(Item):
def use(self) -> None:
"""
When the bomb is used, throw it and explodes it.
When the bomb is used, it is thrown and then it explodes.
"""
if self.held:
self.owner = self.held_by
@ -138,7 +165,7 @@ class Bomb(Item):
def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
Special exploding action of the bomb.
"""
if self.exploding:
if self.tick > 0:
@ -158,9 +185,13 @@ class Bomb(Item):
m.logs.add_message(log_message)
m.entities.remove(self)
# Add sparkles where the bomb exploded.
explosion = Explosion(y=self.y, x=self.x)
self.map.add_entity(explosion)
def save_state(self) -> dict:
"""
Saves the state of the bomb into a dictionary
Saves the state of the bomb into a dictionary.
"""
d = super().save_state()
d["exploding"] = self.exploding
@ -168,6 +199,25 @@ class Bomb(Item):
return d
class Explosion(Item):
"""
When a bomb explodes, the explosion is displayed.
"""
def __init__(self, *args, **kwargs):
super().__init__(name="explosion", *args, **kwargs)
def act(self, m: Map) -> None:
"""
The bomb disappears after exploding.
"""
m.remove_entity(self)
def hold(self, player: InventoryHolder) -> None:
"""
The player can't hold an explosion.
"""
class Weapon(Item):
"""
Non-throwable items that improve player damage
@ -178,6 +228,10 @@ class Weapon(Item):
super().__init__(*args, **kwargs)
self.damage = damage
@property
def description(self) -> str:
return f"STR+{self.damage}" if self.damage else super().description
def save_state(self) -> dict:
"""
Saves the state of the weapon into a dictionary
@ -186,14 +240,101 @@ class Weapon(Item):
d["damage"] = self.damage
return d
def equip(self) -> None:
"""
When a weapon is equipped, the player gains strength.
"""
self.held_by.remove_from_inventory(self)
self.held_by.equipped_main = self
self.held_by.strength += self.damage
def unequip(self) -> None:
"""
Remove the strength earned by the weapon.
:return:
"""
super().unequip()
self.held_by.strength -= self.damage
class Sword(Weapon):
"""
A basic weapon
"""
def __init__(self, name: str = "sword", price: int = 20, *args, **kwargs):
def __init__(self, name: str = "sword", price: int = 20,
*args, **kwargs):
super().__init__(name=name, price=price, *args, **kwargs)
self.name = name
class Armor(Item):
"""
Class of items that increase the player's constitution.
"""
constitution: int
def __init__(self, constitution: int, *args, **kwargs):
super().__init__(*args, **kwargs)
self.constitution = constitution
@property
def description(self) -> str:
return f"CON+{self.constitution}" if self.constitution \
else super().description
def equip(self) -> None:
super().equip()
self.held_by.constitution += self.constitution
def unequip(self) -> None:
super().unequip()
self.held_by.constitution -= self.constitution
def save_state(self) -> dict:
d = super().save_state()
d["constitution"] = self.constitution
return d
class Shield(Armor):
"""
Class of shield items, they can be equipped in the other hand.
"""
def __init__(self, name: str = "shield", constitution: int = 2,
price: int = 16, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price,
*args, **kwargs)
class Helmet(Armor):
"""
Class of helmet items, they can be equipped on the head.
"""
def __init__(self, name: str = "helmet", constitution: int = 2,
price: int = 18, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price,
*args, **kwargs)
def equip(self) -> None:
if self.held_by.equipped_helmet:
self.held_by.equipped_helmet.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_helmet = self
class Chestplate(Armor):
"""
Class of chestplate items, they can be equipped on the body.
"""
def __init__(self, name: str = "chestplate", constitution: int = 4,
price: int = 30, *args, **kwargs):
super().__init__(name=name, constitution=constitution, price=price,
*args, **kwargs)
def equip(self) -> None:
if self.held_by.equipped_armor:
self.held_by.equipped_armor.unequip()
self.held_by.remove_from_inventory(self)
self.held_by.equipped_armor = self
class BodySnatchPotion(Item):
@ -228,3 +369,93 @@ class BodySnatchPotion(Item):
self.held_by.recalculate_paths()
self.held_by.inventory.remove(self)
class Ring(Item):
"""
A class of rings that boost the player's statistics.
"""
maxhealth: int
strength: int
intelligence: int
charisma: int
dexterity: int
constitution: int
critical: int
experience: float
def __init__(self, maxhealth: int = 0, strength: int = 0,
intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0,
critical: int = 0, experience: float = 0, *args, **kwargs):
super().__init__(*args, **kwargs)
self.maxhealth = maxhealth
self.strength = strength
self.intelligence = intelligence
self.charisma = charisma
self.dexterity = dexterity
self.constitution = constitution
self.critical = critical
self.experience = experience
@property
def description(self) -> str:
fields = [("MAX HP", self.maxhealth), ("STR", self.strength),
("INT", self.intelligence), ("CHR", self.charisma),
("DEX", self.dexterity), ("CON", self.constitution),
("CRI", self.critical), ("XP", self.experience)]
return ", ".join(f"{key}+{value}" for key, value in fields if value)
def equip(self) -> None:
super().equip()
self.held_by.maxhealth += self.maxhealth
self.held_by.strength += self.strength
self.held_by.intelligence += self.intelligence
self.held_by.charisma += self.charisma
self.held_by.dexterity += self.dexterity
self.held_by.constitution += self.constitution
self.held_by.critical += self.critical
self.held_by.xp_buff += self.experience
def unequip(self) -> None:
super().unequip()
self.held_by.maxhealth -= self.maxhealth
self.held_by.strength -= self.strength
self.held_by.intelligence -= self.intelligence
self.held_by.charisma -= self.charisma
self.held_by.dexterity -= self.dexterity
self.held_by.constitution -= self.constitution
self.held_by.critical -= self.critical
self.held_by.xp_buff -= self.experience
def save_state(self) -> dict:
d = super().save_state()
d["maxhealth"] = self.maxhealth
d["strength"] = self.strength
d["intelligence"] = self.intelligence
d["charisma"] = self.charisma
d["dexterity"] = self.dexterity
d["constitution"] = self.constitution
d["critical"] = self.critical
d["experience"] = self.experience
return d
class RingCritical(Ring):
def __init__(self, name: str = "ring_of_critical_damage", price: int = 15,
critical: int = 20, *args, **kwargs):
super().__init__(name=name, price=price, critical=critical,
*args, **kwargs)
class RingXP(Ring):
def __init__(self, name: str = "ring_of_more_experience", price: int = 25,
experience: float = 2, *args, **kwargs):
super().__init__(name=name, price=price, experience=experience,
*args, **kwargs)
class Monocle(Item):
def __init__(self, name: str = "monocle", price: int = 10,
*args, **kwargs):
super().__init__(name=name, price=price, *args, **kwargs)

View File

@ -10,8 +10,8 @@ from ..interfaces import FightingEntity, Map
class Monster(FightingEntity):
"""
The class for all monsters in the dungeon.
A monster must override this class, and the parameters are given
in the __init__ function.
All specific monster classes overwrite 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:
@ -21,7 +21,7 @@ class Monster(FightingEntity):
super().__init__(name="my_monster", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
With that way, attributes can be overwritten when the entity got created.
With that way, attributes can be overwritten when the entity is created.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -29,7 +29,7 @@ class Monster(FightingEntity):
def act(self, m: Map) -> None:
"""
By default, a monster will move randomly where it is possible
And if a player is close to the monster, the monster run on the player.
If the player is closeby, the monster runs to the player.
"""
target = None
for entity in m.entities:
@ -38,12 +38,12 @@ class Monster(FightingEntity):
target = entity
break
# A Dijkstra algorithm has ran that targets the player.
# With that way, monsters can simply follow the path.
# If they can't move and they are already close to the player,
# They hit.
# Monsters move according to a Dijkstra algorithm
# that targets the player.
# If they can not move and are already close to the player,
# they hit.
if target and (self.y, self.x) in target.paths:
# Move to target player by choosing the best avaliable path
# Moves to target player by choosing the best available path
for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
@ -52,8 +52,8 @@ class Monster(FightingEntity):
self.map.logs.add_message(self.hit(target))
break
else:
# Move in a random direction
# If the direction is not available, try another one
# Moves in a random direction
# If the direction is not available, tries another one
moves = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
@ -61,10 +61,17 @@ class Monster(FightingEntity):
if move():
break
def move(self, y: int, x: int) -> None:
"""
Overwrites the move function to recalculate paths.
"""
super().move(y, x)
self.recalculate_paths()
class Tiger(Monster):
"""
A tiger monster
A tiger monster.
"""
def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None:
@ -74,7 +81,7 @@ class Tiger(Monster):
class Hedgehog(Monster):
"""
A really mean hedgehog monster
A really mean hedgehog monster.
"""
def __init__(self, name: str = "hedgehog", strength: int = 3,
maxhealth: int = 10, *args, **kwargs) -> None:
@ -84,19 +91,31 @@ class Hedgehog(Monster):
class Rabbit(Monster):
"""
A rabbit monster
A rabbit monster.
"""
def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, *args, **kwargs) -> None:
maxhealth: int = 15, critical: int = 30,
*args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
maxhealth=maxhealth, critical=critical,
*args, **kwargs)
class TeddyBear(Monster):
"""
A cute teddybear monster
A cute teddybear monster.
"""
def __init__(self, name: str = "teddy_bear", strength: int = 0,
maxhealth: int = 50, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class GiantSeaEagle(Monster):
"""
An eagle boss
"""
def __init__(self, name: str = "eagle", strength: int = 1000,
maxhealth: int = 5000, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -1,37 +1,54 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from functools import reduce
from queue import PriorityQueue
from random import randint
from typing import Dict, Tuple
from typing import Dict, Optional, Tuple
from .items import Item
from ..interfaces import FightingEntity, InventoryHolder
class Player(InventoryHolder, FightingEntity):
"""
The class of the player
The class of the player.
"""
current_xp: int = 0
max_xp: int = 10
xp_buff: float = 1
paths: Dict[Tuple[int, int], Tuple[int, int]]
equipped_main: Optional[Item]
equipped_secondary: Optional[Item]
equipped_helmet: Optional[Item]
equipped_armor: Optional[Item]
def __init__(self, name: str = "player", maxhealth: int = 20,
strength: int = 5, intelligence: int = 1, charisma: int = 1,
dexterity: int = 1, constitution: int = 1, level: int = 1,
current_xp: int = 0, max_xp: int = 10, inventory: list = None,
hazel: int = 42, *args, **kwargs) \
-> None:
hazel: int = 42, equipped_main: Optional[Item] = None,
equipped_armor: Optional[Item] = None, critical: int = 5,
equipped_secondary: Optional[Item] = None,
equipped_helmet: Optional[Item] = None, xp_buff: float = 1,
vision: int = 5, *args, **kwargs) -> None:
super().__init__(name=name, maxhealth=maxhealth, strength=strength,
intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution,
level=level, *args, **kwargs)
level=level, critical=critical, *args, **kwargs)
self.current_xp = current_xp
self.max_xp = max_xp
self.xp_buff = xp_buff
self.inventory = self.translate_inventory(inventory or [])
self.paths = dict()
self.hazel = hazel
self.equipped_main = self.dict_to_item(equipped_main) \
if isinstance(equipped_main, dict) else equipped_main
self.equipped_armor = self.dict_to_item(equipped_armor) \
if isinstance(equipped_armor, dict) else equipped_armor
self.equipped_secondary = self.dict_to_item(equipped_secondary) \
if isinstance(equipped_secondary, dict) else equipped_secondary
self.equipped_helmet = self.dict_to_item(equipped_helmet) \
if isinstance(equipped_helmet, dict) else equipped_helmet
self.vision = vision
def move(self, y: int, x: int) -> None:
"""
@ -42,10 +59,11 @@ class Player(InventoryHolder, FightingEntity):
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
self.map.compute_visibility(self.y, self.x, self.vision)
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
Add as many levels as possible to the player.
"""
while self.current_xp > self.max_xp:
self.level += 1
@ -59,12 +77,27 @@ class Player(InventoryHolder, FightingEntity):
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
Adds some experience to the player.
If the required amount is reached, the player levels up.
"""
self.current_xp += xp
self.current_xp += int(xp * self.xp_buff)
self.level_up()
def remove_from_inventory(self, obj: Item) -> None:
"""
Remove the given item from the inventory, even if the item is equipped.
"""
if obj == self.equipped_main:
self.equipped_main = None
elif obj == self.equipped_armor:
self.equipped_armor = None
elif obj == self.equipped_secondary:
self.equipped_secondary = None
elif obj == self.equipped_helmet:
self.equipped_helmet = None
else:
return super().remove_from_inventory(obj)
# noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool:
@ -87,56 +120,6 @@ class Player(InventoryHolder, FightingEntity):
entity.hold(self)
return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths for monsters to go to
the player. Actually, the paths are computed for each tile adjacent to
the player then for each step the monsters use the best path avaliable.
"""
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
@ -144,4 +127,12 @@ class Player(InventoryHolder, FightingEntity):
d = super().save_state()
d["current_xp"] = self.current_xp
d["max_xp"] = self.max_xp
d["equipped_main"] = self.equipped_main.save_state()\
if self.equipped_main else None
d["equipped_armor"] = self.equipped_armor.save_state()\
if self.equipped_armor else None
d["equipped_secondary"] = self.equipped_secondary.save_state()\
if self.equipped_secondary else None
d["equipped_helmet"] = self.equipped_helmet.save_state()\
if self.equipped_helmet else None
return d

View File

@ -21,20 +21,20 @@ class DisplayActions(Enum):
class GameMode(Enum):
"""
Game mode options
Game mode options.
"""
MAINMENU = auto()
PLAY = auto()
SETTINGS = auto()
INVENTORY = auto()
STORE = auto()
CREDITS = auto()
class KeyValues(Enum):
"""
Key values options used in the game
Key values options used in the game.
"""
MOUSE = auto()
UP = auto()
DOWN = auto()
LEFT = auto()
@ -46,11 +46,13 @@ class KeyValues(Enum):
DROP = auto()
SPACE = auto()
CHAT = auto()
WAIT = auto()
LADDER = auto()
@staticmethod
def translate_key(key: str, settings: Settings) -> Optional["KeyValues"]:
"""
Translate the raw string key into an enum value that we can use.
Translates the raw string key into an enum value that we can use.
"""
if key in (settings.KEY_DOWN_SECONDARY,
settings.KEY_DOWN_PRIMARY):
@ -78,4 +80,8 @@ class KeyValues(Enum):
return KeyValues.SPACE
elif key == settings.KEY_CHAT:
return KeyValues.CHAT
elif key == settings.KEY_WAIT:
return KeyValues.WAIT
elif key == settings.KEY_LADDER:
return KeyValues.LADDER
return None

View File

@ -3,7 +3,7 @@
from json import JSONDecodeError
from random import randint
from typing import Any, Optional
from typing import Any, Optional, List
import curses
import json
import os
@ -23,7 +23,8 @@ class Game:
"""
The game object controls all actions in the game.
"""
map: Map
maps: List[Map]
map_index: int
player: Player
screen: Any
# display_actions is a display interface set by the bootstrapper
@ -31,10 +32,11 @@ class Game:
def __init__(self) -> None:
"""
Init the game.
Initiates the game.
"""
self.state = GameMode.MAINMENU
self.waiting_for_friendly_key = False
self.is_in_store_menu = True
self.settings = Settings()
self.settings.load_settings()
self.settings.write_settings()
@ -49,8 +51,11 @@ class Game:
def new_game(self) -> None:
"""
Create a new game on the screen.
Creates a new game on the screen.
"""
# TODO generate a new map procedurally
self.maps = []
self.map_index = 0
self.map = broguelike.Generator().run()
self.map.logs = self.logs
self.logs.clear()
@ -60,20 +65,44 @@ class Game:
self.map.spawn_random_entities(randint(3, 10))
self.inventory_menu.update_player(self.player)
def run(self, screen: Any) -> None:
@property
def map(self) -> Map:
"""
Return the current map where the user is.
"""
return self.maps[self.map_index]
@map.setter
def map(self, m: Map) -> None:
"""
Redefine the current map.
"""
if len(self.maps) == self.map_index:
# Insert new map
self.maps.append(m)
# Redefine the current map
self.maps[self.map_index] = m
def run(self, screen: Any) -> None: # pragma no cover
"""
Main infinite loop.
We wait for the player's action, then we do what that should be done
when the given key gets pressed.
We wait for the player's action, then we do what should be done
when a key gets pressed.
"""
while True: # pragma no cover
screen.refresh()
while True:
screen.erase()
screen.refresh()
screen.noutrefresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
curses.doupdate()
try:
key = screen.getkey()
except KeyboardInterrupt:
exit(0)
return
if key == "KEY_MOUSE":
_ignored1, x, y, _ignored2, _ignored3 = curses.getmouse()
self.display_actions(DisplayActions.MOUSE, y, x)
_ignored1, x, y, _ignored2, attr = curses.getmouse()
self.display_actions(DisplayActions.MOUSE, y, x, attr)
else:
self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
@ -81,7 +110,7 @@ class Game:
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None:
"""
Indicates what should be done when the given key is pressed,
Indicates what should be done when a given key is pressed,
according to the current game state.
"""
if self.message:
@ -103,36 +132,95 @@ class Game:
self.settings_menu.handle_key_pressed(key, raw_key, self)
elif self.state == GameMode.STORE:
self.handle_key_pressed_store(key)
elif self.state == GameMode.CREDITS:
self.state = GameMode.MAINMENU
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
def handle_key_pressed_play(self, key: KeyValues) -> None: # noqa: C901
"""
In play mode, arrows or zqsd move the main character.
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
self.map.tick(self.player)
elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY
self.display_actions(DisplayActions.UPDATE)
elif key == KeyValues.USE and self.player.equipped_main:
if self.player.equipped_main:
self.player.equipped_main.use()
if self.player.equipped_secondary:
self.player.equipped_secondary.use()
elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU
elif key == KeyValues.CHAT:
# Wait for the direction of the friendly entity
self.waiting_for_friendly_key = True
elif key == KeyValues.WAIT:
self.map.tick(self.player)
elif key == KeyValues.LADDER:
self.handle_ladder()
def handle_ladder(self) -> None:
"""
The player pressed the ladder key to switch map
"""
# On a ladder, we switch level
y, x = self.player.y, self.player.x
if not self.map.tiles[y][x].is_ladder():
return
# We move up on the ladder of the beginning,
# down at the end of the stage
move_down = y != self.map.start_y and x != self.map.start_x
old_map = self.map
self.map_index += 1 if move_down else -1
if self.map_index == -1:
self.map_index = 0
return
while self.map_index >= len(self.maps):
# TODO: generate a new map
self.maps.append(Map.load(ResourceManager.get_asset_path(
"example_map_2.txt")))
new_map = self.map
new_map.floor = self.map_index
old_map.remove_entity(self.player)
new_map.add_entity(self.player)
if move_down:
self.player.move(self.map.start_y, self.map.start_x)
self.logs.add_message(
_("The player climbs down to the floor {floor}.")
.format(floor=-self.map_index))
else:
# Find the ladder of the end of the game
ladder_y, ladder_x = -1, -1
for y in range(self.map.height):
for x in range(self.map.width):
if (y, x) != (self.map.start_y, self.map.start_x) \
and self.map.tiles[y][x].is_ladder():
ladder_y, ladder_x = y, x
break
self.player.move(ladder_y, ladder_x)
self.logs.add_message(
_("The player climbs up the floor {floor}.")
.format(floor=-self.map_index))
self.display_actions(DisplayActions.UPDATE)
def handle_friendly_entity_chat(self, key: KeyValues) -> None:
"""
If the player is talking to a friendly entity, we get the direction
where the entity is, then we interact with it.
If the player tries to talk to a friendly entity, the game waits for
a directional key to be pressed, verifies there is a friendly entity
in that direction and then lets the player interact with it.
"""
if not self.waiting_for_friendly_key:
return
@ -160,7 +248,9 @@ class Game:
self.logs.add_message(msg)
if entity.is_merchant():
self.state = GameMode.STORE
self.is_in_store_menu = True
self.store_menu.update_merchant(entity)
self.display_actions(DisplayActions.UPDATE)
def handle_key_pressed_inventory(self, key: KeyValues) -> None:
"""
@ -189,26 +279,37 @@ class Game:
"""
In a store menu, we can buy items or close the menu.
"""
if key == KeyValues.SPACE:
menu = self.store_menu if self.is_in_store_menu else self.inventory_menu
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
self.state = GameMode.PLAY
elif key == KeyValues.UP:
self.store_menu.go_up()
menu.go_up()
elif key == KeyValues.DOWN:
self.store_menu.go_down()
if self.store_menu.values and not self.player.dead:
menu.go_down()
elif key == KeyValues.LEFT:
self.is_in_store_menu = False
self.display_actions(DisplayActions.UPDATE)
elif key == KeyValues.RIGHT:
self.is_in_store_menu = True
self.display_actions(DisplayActions.UPDATE)
if menu.values and not self.player.dead:
if key == KeyValues.ENTER:
item = self.store_menu.validate()
flag = item.be_sold(self.player, self.store_menu.merchant)
item = menu.validate()
owner = self.store_menu.merchant if self.is_in_store_menu \
else self.player
buyer = self.player if self.is_in_store_menu \
else self.store_menu.merchant
flag = item.be_sold(buyer, owner)
if not flag:
self.message = _("You do not have enough money")
self.display_actions(DisplayActions.UPDATE)
self.message = _("The buyer does not have enough money")
self.display_actions(DisplayActions.UPDATE)
# Ensure that the cursor has a good position
self.store_menu.position = min(self.store_menu.position,
len(self.store_menu.values) - 1)
menu.position = min(menu.position, len(menu.values) - 1)
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
"""
In the main menu, we can navigate through options.
In the main menu, we can navigate through different options.
"""
if key == KeyValues.DOWN:
self.main_menu.go_down()
@ -233,16 +334,18 @@ class Game:
def save_state(self) -> dict:
"""
Saves the game to a dictionary
Saves the game to a dictionary.
"""
return self.map.save_state()
return dict(map_index=self.map_index,
maps=[m.save_state() for m in self.maps])
def load_state(self, d: dict) -> None:
"""
Loads the game from a dictionary
Loads the game from a dictionary.
"""
try:
self.map.load_state(d)
self.map_index = d["map_index"]
self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]]
except KeyError:
self.message = _("Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted.")
@ -259,11 +362,13 @@ class Game:
return
self.player = players[0]
self.map.compute_visibility(self.player.y, self.player.x,
self.player.vision)
self.display_actions(DisplayActions.UPDATE)
def load_game(self) -> None:
"""
Loads the game from a file
Loads the game from a file.
"""
file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path):
@ -280,7 +385,7 @@ class Game:
def save_game(self) -> None:
"""
Saves the game to a file
Saves the game to a file.
"""
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write(json.dumps(self.save_state()))

View File

@ -2,10 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List, Optional, Any
from math import ceil, sqrt
from itertools import product
from random import choice, choices, randint
from typing import List, Optional, Any, Dict, Tuple
from queue import PriorityQueue
from functools import reduce
from .display.texturepack import TexturePack
from .translations import gettext as _
@ -13,7 +15,7 @@ from .translations import gettext as _
class Logs:
"""
The logs object stores the messages to display. It is encapsulating a list
The logs object stores the messages to display. It encapsulates a list
of such messages, to allow multiple pointers to keep track of it even if
the list was to be reassigned.
"""
@ -31,16 +33,47 @@ class Logs:
self.messages = []
class Slope():
X: int
Y: int
def __init__(self, y: int, x: int) -> None:
self.Y = y
self.X = x
def compare(self, other: "Slope") -> int:
y, x = other.Y, other.X
return self.Y * x - self.X * y
def __lt__(self, other: "Slope") -> bool:
return self.compare(other) < 0
def __eq__(self, other: "Slope") -> bool:
return self.compare(other) == 0
def __gt__(self, other: "Slope") -> bool:
return self.compare(other) > 0
def __le__(self, other: "Slope") -> bool:
return self.compare(other) <= 0
def __ge__(self, other: "Slope") -> bool:
return self.compare(other) >= 0
class Map:
"""
Object that represents a Map with its width, height
The Map object represents a with its width, height
and tiles, that have their custom properties.
"""
floor: int
width: int
height: int
start_y: int
start_x: int
tiles: List[List["Tile"]]
visibility: List[List[bool]]
seen_tiles: List[List[bool]]
entities: List["Entity"]
logs: Logs
# coordinates of the point that should be
@ -48,28 +81,36 @@ class Map:
currentx: int
currenty: int
def __init__(self, width: int, height: int, tiles: list,
start_y: int, start_x: int):
def __init__(self, width: int = 0, height: int = 0, tiles: list = None,
start_y: int = 0, start_x: int = 0):
self.floor = 0
self.width = width
self.height = height
self.start_y = start_y
self.start_x = start_x
self.currenty = start_y
self.currentx = start_x
self.tiles = tiles
self.tiles = tiles or []
self.visibility = [[False for _ in range(len(self.tiles[0]))]
for _ in range(len(self.tiles))]
self.seen_tiles = [[False for _ in range(len(tiles[0]))]
for _ in range(len(self.tiles))]
self.entities = []
self.logs = Logs()
def add_entity(self, entity: "Entity") -> None:
"""
Register a new entity in the map.
Registers a new entity in the map.
"""
self.entities.append(entity)
if entity.is_familiar():
self.entities.insert(1, entity)
else:
self.entities.append(entity)
entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
Unregisters an entity from the map.
"""
if entity in self.entities:
self.entities.remove(entity)
@ -89,7 +130,7 @@ class Map:
def entity_is_present(self, y: int, x: int) -> bool:
"""
Indicates that the tile at the coordinates (y, x) contains a killable
entity
entity.
"""
return 0 <= y < self.height and 0 <= x < self.width and \
any(entity.x == x and entity.y == y and entity.is_friendly()
@ -98,7 +139,8 @@ class Map:
@staticmethod
def load(filename: str) -> "Map":
"""
Read a file that contains the content of a map, and build a Map object.
Reads a file that contains the content of a map,
and builds a Map object.
"""
with open(filename, "r") as f:
file = f.read()
@ -107,7 +149,7 @@ class Map:
@staticmethod
def load_from_string(content: str) -> "Map":
"""
Load a map represented by its characters and build a Map object.
Loads a map represented by its characters and builds a Map object.
"""
lines = content.split("\n")
first_line = lines[0]
@ -123,7 +165,7 @@ class Map:
@staticmethod
def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
"""
Transforms a string into the list of corresponding tiles
Transforms a string into the list of corresponding tiles.
"""
lines = content.split("\n")
tiles = [[Tile.from_ascii_char(c)
@ -132,7 +174,7 @@ class Map:
def draw_string(self, pack: TexturePack) -> str:
"""
Draw the current map as a string object that can be rendered
Draws the current map as a string object that can be rendered
in the window.
"""
return "\n".join("".join(tile.char(pack) for tile in line)
@ -140,29 +182,153 @@ class Map:
def spawn_random_entities(self, count: int) -> None:
"""
Put randomly {count} entities on the map, where it is available.
Puts randomly {count} entities on the map, only on empty ground tiles.
"""
for ignored in range(count):
for _ignored in range(count):
y, x = 0, 0
while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
tile = self.tiles[y][x]
if tile.can_walk():
break
entity = choice(Entity.get_all_entity_classes())()
entity = choices(Entity.get_all_entity_classes(),
weights=Entity.get_weights(), k=1)[0]()
entity.move(y, x)
self.add_entity(entity)
def tick(self) -> None:
def compute_visibility(self, y: int, x: int, max_range: int) -> None:
"""
Trigger all entity events.
Sets the visible tiles to be the ones visible by an entity at point
(y, x), using a twaked shadow casting algorithm
"""
for line in self.visibility:
for i in range(len(line)):
line[i] = False
self.set_visible(0, 0, 0, (y, x))
for octant in range(8):
self.compute_visibility_octant(octant, (y, x), max_range, 1,
Slope(1, 1), Slope(0, 1))
def crop_top_visibility(self, octant: int, origin: Tuple[int, int],
x: int, top: Slope) -> int:
if top.X == 1:
top_y = x
else:
top_y = ceil(((x * 2 - 1) * top.Y + top.X) / (top.X * 2))
if self.is_wall(top_y, x, octant, origin):
top_y += top >= Slope(top_y * 2 + 1, x * 2) and not \
self.is_wall(top_y + 1, x, octant, origin)
else:
ax = x * 2
ax += self.is_wall(top_y + 1, x + 1, octant, origin)
top_y += top > Slope(top_y * 2 + 1, ax)
return top_y
def crop_bottom_visibility(self, octant: int, origin: Tuple[int, int],
x: int, bottom: Slope) -> int:
if bottom.Y == 0:
bottom_y = 0
else:
bottom_y = ceil(((x * 2 - 1) * bottom.Y + bottom.X)
/ (bottom.X * 2))
bottom_y += bottom >= Slope(bottom_y * 2 + 1, x * 2) and \
self.is_wall(bottom_y, x, octant, origin) and \
not self.is_wall(bottom_y + 1, x, octant, origin)
return bottom_y
def compute_visibility_octant(self, octant: int, origin: Tuple[int, int],
max_range: int, distance: int, top: Slope,
bottom: Slope) -> None:
for x in range(distance, max_range + 1):
top_y = self.crop_top_visibility(octant, origin, x, top)
bottom_y = self.crop_bottom_visibility(octant, origin, x, bottom)
was_opaque = -1
for y in range(top_y, bottom_y - 1, -1):
if x + y > max_range:
continue
is_opaque = self.is_wall(y, x, octant, origin)
is_visible = is_opaque\
or ((y != top_y or top > Slope(y * 4 - 1, x * 4 + 1))
and (y != bottom_y
or bottom < Slope(y * 4 + 1, x * 4 - 1)))
# is_visible = is_opaque\
# or ((y != top_y or top >= Slope(y, x))
# and (y != bottom_y or bottom <= Slope(y, x)))
if is_visible:
self.set_visible(y, x, octant, origin)
if x == max_range:
continue
if is_opaque and was_opaque == 0:
nx, ny = x * 2, y * 2 + 1
nx -= self.is_wall(y + 1, x, octant, origin)
if top > Slope(ny, nx):
if y == bottom_y:
bottom = Slope(ny, nx)
break
else:
self.compute_visibility_octant(
octant, origin, max_range, x + 1, top,
Slope(ny, nx))
elif y == bottom_y: # pragma: no cover
return
elif not is_opaque and was_opaque == 1:
nx, ny = x * 2, y * 2 + 1
nx += self.is_wall(y + 1, x + 1, octant, origin)
if bottom >= Slope(ny, nx): # pragma: no cover
return
top = Slope(ny, nx)
was_opaque = is_opaque
if was_opaque != 0:
break
@staticmethod
def translate_coord(y: int, x: int, octant: int,
origin: Tuple[int, int]) -> Tuple[int, int]:
ny, nx = origin
if octant == 0:
return ny - y, nx + x
elif octant == 1:
return ny - x, nx + y
elif octant == 2:
return ny - x, nx - y
elif octant == 3:
return ny - y, nx - x
elif octant == 4:
return ny + y, nx - x
elif octant == 5:
return ny + x, nx - y
elif octant == 6:
return ny + x, nx + y
elif octant == 7:
return ny + y, nx + x
def is_wall(self, y: int, x: int, octant: int,
origin: Tuple[int, int]) -> bool:
y, x = self.translate_coord(y, x, octant, origin)
return 0 <= y < len(self.tiles) and 0 <= x < len(self.tiles[0]) and \
self.tiles[y][x].is_wall()
def set_visible(self, y: int, x: int, octant: int,
origin: Tuple[int, int]) -> None:
y, x = self.translate_coord(y, x, octant, origin)
if 0 <= y < len(self.tiles) and 0 <= x < len(self.tiles[0]):
self.visibility[y][x] = True
self.seen_tiles[y][x] = True
def tick(self, p: Any) -> None:
"""
Triggers all entity events.
"""
for entity in self.entities:
entity.act(self)
if entity.is_familiar():
entity.act(p, self)
else:
entity.act(self)
def save_state(self) -> dict:
"""
Saves the map's attributes to a dictionary
Saves the map's attributes to a dictionary.
"""
d = dict()
d["width"] = self.width
@ -175,11 +341,12 @@ class Map:
for enti in self.entities:
d["entities"].append(enti.save_state())
d["map"] = self.draw_string(TexturePack.ASCII_PACK)
d["seen_tiles"] = self.seen_tiles
return d
def load_state(self, d: dict) -> None:
def load_state(self, d: dict) -> "Map":
"""
Loads the map's attributes from a dictionary
Loads the map's attributes from a dictionary.
"""
self.width = d["width"]
self.height = d["height"]
@ -188,11 +355,16 @@ class Map:
self.currentx = d["currentx"]
self.currenty = d["currenty"]
self.tiles = self.load_dungeon_from_string(d["map"])
self.seen_tiles = d["seen_tiles"]
self.visibility = [[False for _ in range(len(self.tiles[0]))]
for _ in range(len(self.tiles))]
self.entities = []
dictclasses = Entity.get_all_entity_classes_in_a_dict()
for entisave in d["entities"]:
self.add_entity(dictclasses[entisave["type"]](**entisave))
return self
@staticmethod
def neighbourhood(grid: List[List["Tile"]], y: int, x: int,
large: bool = False, oob: bool = False) \
@ -217,16 +389,17 @@ class Map:
class Tile(Enum):
"""
The internal representation of the tiles of the map
The internal representation of the tiles of the map.
"""
EMPTY = auto()
WALL = auto()
FLOOR = auto()
LADDER = auto()
@staticmethod
def from_ascii_char(ch: str) -> "Tile":
"""
Maps an ascii character to its equivalent in the texture pack
Maps an ascii character to its equivalent in the texture pack.
"""
for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch:
@ -236,9 +409,27 @@ class Tile(Enum):
def char(self, pack: TexturePack) -> str:
"""
Translates a Tile to the corresponding character according
to the texture pack
to the texture pack.
"""
return getattr(pack, self.name)
val = getattr(pack, self.name)
return val[0] if isinstance(val, tuple) else val
def visible_color(self, pack: TexturePack) -> Tuple[int, int]:
"""
Retrieve the tuple (fg_color, bg_color) of the current Tile
if it is visible.
"""
val = getattr(pack, self.name)
return (val[2], val[4]) if isinstance(val, tuple) else \
(pack.tile_fg_visible_color, pack.tile_bg_color)
def hidden_color(self, pack: TexturePack) -> Tuple[int, int]:
"""
Retrieve the tuple (fg_color, bg_color) of the current Tile.
"""
val = getattr(pack, self.name)
return (val[1], val[3]) if isinstance(val, tuple) else \
(pack.tile_fg_color, pack.tile_bg_color)
def is_wall(self) -> bool:
"""
@ -246,21 +437,28 @@ class Tile(Enum):
"""
return self == Tile.WALL
def is_ladder(self) -> bool:
"""
Is this Tile a ladder?
"""
return self == Tile.LADDER
def can_walk(self) -> bool:
"""
Check if an entity (player or not) can move in this tile.
Checks if an entity (player or not) can move in this tile.
"""
return not self.is_wall() and self != Tile.EMPTY
class Entity:
"""
An Entity object represents any entity present on the map
An Entity object represents any entity present on the map.
"""
y: int
x: int
name: str
map: Map
paths: Dict[Tuple[int, int], Tuple[int, int]]
# noinspection PyShadowingBuiltins
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
@ -269,11 +467,12 @@ class Entity:
self.x = x
self.name = name
self.map = map
self.paths = None
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool:
"""
Checks if moving to (y,x) is authorized
Checks if moving to (y,x) is authorized.
"""
free = self.map.is_free(y, x)
if free and move_if_possible:
@ -282,7 +481,7 @@ class Entity:
def move(self, y: int, x: int) -> bool:
"""
Moves an entity to (y,x) coordinates
Moves an entity to (y,x) coordinates.
"""
self.y = y
self.x = x
@ -290,49 +489,100 @@ class Entity:
def move_up(self, force: bool = False) -> bool:
"""
Moves the entity up one tile, if possible
Moves the entity up one tile, if possible.
"""
return self.move(self.y - 1, self.x) if force else \
self.check_move(self.y - 1, self.x, True)
def move_down(self, force: bool = False) -> bool:
"""
Moves the entity down one tile, if possible
Moves the entity down one tile, if possible.
"""
return self.move(self.y + 1, self.x) if force else \
self.check_move(self.y + 1, self.x, True)
def move_left(self, force: bool = False) -> bool:
"""
Moves the entity left one tile, if possible
Moves the entity left one tile, if possible.
"""
return self.move(self.y, self.x - 1) if force else \
self.check_move(self.y, self.x - 1, True)
def move_right(self, force: bool = False) -> bool:
"""
Moves the entity right one tile, if possible
Moves the entity right one tile, if possible.
"""
return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True)
def recalculate_paths(self, max_distance: int = 12) -> None:
"""
Uses Dijkstra algorithm to calculate best paths for other entities to
go to this entity. If self.paths is None, does nothing.
"""
if self.paths is None:
return
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
def act(self, m: Map) -> None:
"""
Define the action of the entity that is ran each tick.
Defines the action the entity will do at each tick.
By default, does nothing.
"""
pass
def distance_squared(self, other: "Entity") -> int:
"""
Get the square of the distance to another entity.
Useful to check distances since square root takes time.
Gives the square of the distance to another entity.
Useful to check distances since taking the square root takes time.
"""
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
def distance(self, other: "Entity") -> float:
"""
Get the cartesian distance to another entity.
Gives the cartesian distance to another entity.
"""
return sqrt(self.distance_squared(other))
@ -355,6 +605,13 @@ class Entity:
"""
return isinstance(self, FriendlyEntity)
def is_familiar(self) -> bool:
"""
Is this entity a familiar?
"""
from squirrelbattle.entities.friendly import Familiar
return isinstance(self, Familiar)
def is_merchant(self) -> bool:
"""
Is this entity a merchant?
@ -364,48 +621,71 @@ class Entity:
@property
def translated_name(self) -> str:
"""
Translates the name of entities.
"""
return _(self.name.replace("_", " "))
@staticmethod
def get_all_entity_classes() -> list:
"""
Returns all entities subclasses
Returns all entities subclasses.
"""
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Merchant, Sunflower
Rabbit, TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet
return [BodySnatchPotion, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,
Sunflower, Tiger, Merchant]
Sunflower, Tiger, Merchant, GiantSeaEagle, Trumpet]
@staticmethod
def get_weights() -> list:
"""
Returns a weigth list associated to the above function, to
be used to spawn random entities with a certain probability.
"""
return [3, 5, 6, 5, 5, 5,
5, 4, 4, 1, 2]
@staticmethod
def get_all_entity_classes_in_a_dict() -> dict:
"""
Returns all entities subclasses in a dictionary
Returns all entities subclasses in a dictionary.
"""
from squirrelbattle.entities.player import Player
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear
from squirrelbattle.entities.friendly import Merchant, Sunflower
TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Merchant, Sunflower, \
Trumpet
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, \
Heart, Sword
Heart, Monocle, Sword, Shield, Chestplate, Helmet, \
RingCritical, RingXP
return {
"Tiger": Tiger,
"Bomb": Bomb,
"Chestplate": Chestplate,
"Heart": Heart,
"BodySnatchPotion": BodySnatchPotion,
"Eagle": GiantSeaEagle,
"Hedgehog": Hedgehog,
"Rabbit": Rabbit,
"TeddyBear": TeddyBear,
"Helmet": Helmet,
"Player": Player,
"Merchant": Merchant,
"Monocle": Monocle,
"Sunflower": Sunflower,
"Sword": Sword,
"Trumpet": Trumpet,
"Shield": Shield,
"TeddyBear": TeddyBear,
"Tiger": Tiger,
"Rabbit": Rabbit,
"RingCritical": RingCritical,
"RingXP": RingXP,
}
def save_state(self) -> dict:
"""
Saves the coordinates of the entity
Saves the coordinates of the entity.
"""
d = dict()
d["x"] = self.x
@ -417,7 +697,7 @@ class Entity:
class FightingEntity(Entity):
"""
A FightingEntity is an entity that can fight, and thus has a health,
level and stats
level and stats.
"""
maxhealth: int
health: int
@ -427,11 +707,12 @@ class FightingEntity(Entity):
dexterity: int
constitution: int
level: int
critical: int
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
strength: int = 0, intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0, level: int = 0,
*args, **kwargs) -> None:
critical: int = 0, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.maxhealth = maxhealth
self.health = maxhealth if health is None else health
@ -441,49 +722,62 @@ class FightingEntity(Entity):
self.dexterity = dexterity
self.constitution = constitution
self.level = level
self.critical = critical
@property
def dead(self) -> bool:
"""
Is this entity dead ?
"""
return self.health <= 0
def hit(self, opponent: "FightingEntity") -> str:
"""
Deals damage to the opponent, based on the stats
The entity deals damage to the opponent
based on their respective stats.
"""
diceroll = randint(1, 100)
damage = self.strength
string = " "
if diceroll <= self.critical: # It is a critical hit
damage *= 4
string = " " + _("It's a critical hit!") + " "
return _("{name} hits {opponent}.")\
.format(name=_(self.translated_name.capitalize()),
opponent=_(opponent.translated_name)) + " " + \
opponent.take_damage(self, self.strength)
opponent=_(opponent.translated_name)) + string + \
opponent.take_damage(self, damage)
def take_damage(self, attacker: "Entity", amount: int) -> str:
"""
Take damage from the attacker, based on the stats
The entity takes damage from the attacker
based on their respective stats.
"""
self.health -= amount
damage = max(0, amount - self.constitution)
self.health -= damage
if self.health <= 0:
self.die()
return _("{name} takes {amount} damage.")\
.format(name=self.translated_name.capitalize(), amount=str(amount))\
return _("{name} takes {damage} damage.")\
.format(name=self.translated_name.capitalize(), damage=str(damage))\
+ (" " + _("{name} dies.")
.format(name=self.translated_name.capitalize())
if self.health <= 0 else "")
def die(self) -> None:
"""
If a fighting entity has no more health, it dies and is removed
If a fighting entity has no more health, it dies and is removed.
"""
self.map.remove_entity(self)
def keys(self) -> list:
"""
Returns a fighting entity's specific attributes
Returns a fighting entity's specific attributes.
"""
return ["name", "maxhealth", "health", "level", "strength",
"intelligence", "charisma", "dexterity", "constitution"]
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
Saves the state of the entity into a dictionary.
"""
d = super().save_state()
for name in self.keys():
@ -493,18 +787,18 @@ class FightingEntity(Entity):
class FriendlyEntity(FightingEntity):
"""
Friendly entities are living entities which do not attack the player
Friendly entities are living entities which do not attack the player.
"""
dialogue_option: list
def talk_to(self, player: Any) -> str:
a = randint(0, len(self.dialogue_option) - 1)
return "The " + self.translated_name \
+ " said : " + self.dialogue_option[a]
return _("{entity} said: {message}").format(
entity=self.translated_name.capitalize(),
message=choice(self.dialogue_option))
def keys(self) -> list:
"""
Returns a friendly entity's specific attributes
Returns a friendly entity's specific attributes.
"""
return ["maxhealth", "health"]
@ -515,17 +809,17 @@ class InventoryHolder(Entity):
def translate_inventory(self, inventory: list) -> list:
"""
Translate the JSON-state of the inventory into a list of the items in
Translates the JSON save of the inventory into a list of the items in
the inventory.
"""
for i in range(len(inventory)):
if isinstance(inventory[i], dict):
inventory[i] = self.dict_to_inventory(inventory[i])
inventory[i] = self.dict_to_item(inventory[i])
return inventory
def dict_to_inventory(self, item_dict: dict) -> Entity:
def dict_to_item(self, item_dict: dict) -> Entity:
"""
Translate a dict object that contains the state of an item
Translates a dictionnary that contains the state of an item
into an item object.
"""
entity_classes = self.get_all_entity_classes_in_a_dict()
@ -535,7 +829,7 @@ class InventoryHolder(Entity):
def save_state(self) -> dict:
"""
We save the inventory of the merchant formatted as JSON
The inventory of the merchant is saved in a JSON format.
"""
d = super().save_state()
d["hazel"] = self.hazel
@ -544,19 +838,21 @@ class InventoryHolder(Entity):
def add_to_inventory(self, obj: Any) -> None:
"""
Adds an object to inventory
Adds an object to the inventory.
"""
self.inventory.append(obj)
if obj not in self.inventory:
self.inventory.append(obj)
def remove_from_inventory(self, obj: Any) -> None:
"""
Removes an object from the inventory
Removes an object from the inventory.
"""
self.inventory.remove(obj)
if obj in self.inventory:
self.inventory.remove(obj)
def change_hazel_balance(self, hz: int) -> None:
"""
Change the number of hazel the entity has by hz. hz is negative
when the player loses money and positive when he gains money
Changes the number of hazel the entity has by hz. hz is negative
when the entity loses money and positive when it gains money.
"""
self.hazel += hz

View File

@ -1,12 +1,14 @@
# German translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao
# This file is distributed under the same license as the squirrelbattle package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-11 18:06+0100\n"
"POT-Creation-Date: 2021-01-08 15:15+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -15,52 +17,116 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/menudisplay.py:113
msgid "== INVENTORY =="
msgstr "== BESTAND =="
msgid "ring_of_critical_damage"
msgstr ""
#: squirrelbattle/display/menudisplay.py:134
msgid "== STALL =="
msgstr "== STAND =="
msgid "ring_of_more_experience"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:34
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
#: squirrelbattle/display/creditsdisplay.py:28
#: squirrelbattle/display/menudisplay.py:123
#: squirrelbattle/display/menudisplay.py:148
msgid "Credits"
msgstr ""
#: squirrelbattle/display/creditsdisplay.py:32
msgid "Developers:"
msgstr ""
#: squirrelbattle/display/creditsdisplay.py:38
msgid "Translators:"
msgstr ""
#: squirrelbattle/display/menudisplay.py:168
msgid "INVENTORY"
msgstr "BESTAND"
#: squirrelbattle/display/menudisplay.py:214
msgid "STALL"
msgstr "STAND"
#: squirrelbattle/display/statsdisplay.py:44
msgid "Inventory:"
msgstr "Bestand:"
#: squirrelbattle/display/statsdisplay.py:53
#: squirrelbattle/display/statsdisplay.py:61
msgid "Equipped main:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:65
msgid "Equipped secondary:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:70
msgid "Equipped chestplate:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:74
msgid "Equipped helmet:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:81
msgid "YOU ARE DEAD"
msgstr "SIE WURDEN GESTORBEN"
#: squirrelbattle/display/statsdisplay.py:85
#, python-brace-format
msgid "Use {key} to use the ladder"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:94
msgid "Move to the friendly entity to talk to it"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:96
#, python-brace-format
msgid "Use {key} then move to talk to the entity"
msgstr ""
#. TODO
#: squirrelbattle/entities/friendly.py:33
msgid "I don't sell any squirrel"
msgstr "Ich verkaufe keinen Eichhörnchen."
#: squirrelbattle/entities/friendly.py:46
#: squirrelbattle/entities/friendly.py:55
msgid "Flower power!!"
msgstr "Blumenmacht!!"
#: squirrelbattle/entities/friendly.py:46
#: squirrelbattle/entities/friendly.py:55
msgid "The sun is warm today"
msgstr "Die Sonne ist warm heute"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:151
#: squirrelbattle/entities/items.py:178
msgid "Bomb is exploding."
msgstr "Die Bombe explodiert."
#: squirrelbattle/entities/items.py:224
#: squirrelbattle/entities/items.py:365
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} täuscht seinem Körper mit {entity} aus."
#: squirrelbattle/game.py:199 squirrelbattle/tests/game_test.py:537
msgid "You do not have enough money"
msgstr ""
#: squirrelbattle/game.py:200
#, python-brace-format
msgid "The player climbs down to the floor {floor}."
msgstr "Der Spieler klettert auf dem Stock {floor} hinunter."
#: squirrelbattle/game.py:243
#: squirrelbattle/game.py:213
#, python-brace-format
msgid "The player climbs up the floor {floor}."
msgstr "Der Spieler klettert auf dem Stock {floor} hinoben."
#: squirrelbattle/game.py:304 squirrelbattle/tests/game_test.py:603
msgid "The buyer does not have enough money"
msgstr "Der Kaufer hat nicht genug Geld"
#: squirrelbattle/game.py:349
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
@ -68,7 +134,7 @@ msgstr ""
"In Ihrer Speicherdatei fehlen einige Schlüssel.\n"
"Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht."
#: squirrelbattle/game.py:251
#: squirrelbattle/game.py:357
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
@ -76,7 +142,7 @@ msgstr ""
"Auf dieser Karte wurde kein Spieler gefunden!\n"
"Vielleicht sind Sie gestorben?"
#: squirrelbattle/game.py:271
#: squirrelbattle/game.py:379
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
@ -84,27 +150,36 @@ msgstr ""
"Die JSON-Datei ist nicht korrekt.\n"
"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht."
#: squirrelbattle/interfaces.py:429
#: squirrelbattle/interfaces.py:718
msgid "It's a critical hit!"
msgstr ""
#: squirrelbattle/interfaces.py:719
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} schlägt {opponent}."
#: squirrelbattle/interfaces.py:441
#: squirrelbattle/interfaces.py:733
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
msgid "{name} takes {damage} damage."
msgstr ""
#: squirrelbattle/interfaces.py:443
#: squirrelbattle/interfaces.py:735
#, python-brace-format
msgid "{name} dies."
msgstr "{name} stirbt."
#: squirrelbattle/interfaces.py:769
#, python-brace-format
msgid "{entity} said: {message}"
msgstr "{entity} hat gesagt: {message}"
#: squirrelbattle/menus.py:73
msgid "Back"
msgstr "Zurück"
#: squirrelbattle/tests/game_test.py:314 squirrelbattle/tests/game_test.py:317
#: squirrelbattle/tests/game_test.py:320
#: squirrelbattle/tests/game_test.py:368 squirrelbattle/tests/game_test.py:371
#: squirrelbattle/tests/game_test.py:374 squirrelbattle/tests/game_test.py:377
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Neu Spiel"
@ -186,45 +261,93 @@ msgid "Key used to talk to a friendly entity"
msgstr "Taste um mit einer friedlicher Entität zu sprechen"
#: squirrelbattle/tests/translations_test.py:55
msgid "Key used to wait"
msgstr "Wartentaste"
#: squirrelbattle/tests/translations_test.py:56
msgid "Key used to use ladders"
msgstr "Leitertaste"
#: squirrelbattle/tests/translations_test.py:58
msgid "Texture pack"
msgstr "Textur-Packung"
#: squirrelbattle/tests/translations_test.py:56
#: squirrelbattle/tests/translations_test.py:59
msgid "Language"
msgstr "Sprache"
#: squirrelbattle/tests/translations_test.py:59
#: squirrelbattle/tests/translations_test.py:62
msgid "player"
msgstr "Spieler"
#: squirrelbattle/tests/translations_test.py:61
msgid "tiger"
msgstr "Tiger"
#: squirrelbattle/tests/translations_test.py:62
#: squirrelbattle/tests/translations_test.py:64
msgid "hedgehog"
msgstr "Igel"
#: squirrelbattle/tests/translations_test.py:63
#: squirrelbattle/tests/translations_test.py:65
msgid "merchant"
msgstr "Kaufmann"
#: squirrelbattle/tests/translations_test.py:66
msgid "rabbit"
msgstr "Kanninchen"
#: squirrelbattle/tests/translations_test.py:64
#: squirrelbattle/tests/translations_test.py:67
msgid "sunflower"
msgstr "Sonnenblume"
#: squirrelbattle/tests/translations_test.py:68
msgid "teddy bear"
msgstr "Teddybär"
#: squirrelbattle/tests/translations_test.py:66
#: squirrelbattle/tests/translations_test.py:69
msgid "tiger"
msgstr "Tiger"
#: squirrelbattle/tests/translations_test.py:70
msgid "eagle"
msgstr ""
#: squirrelbattle/tests/translations_test.py:72
msgid "body snatch potion"
msgstr "Leichenfleddererzaubertrank"
#: squirrelbattle/tests/translations_test.py:67
#: squirrelbattle/tests/translations_test.py:73
msgid "bomb"
msgstr "Bombe"
#: squirrelbattle/tests/translations_test.py:68
#: squirrelbattle/tests/translations_test.py:74
msgid "explosion"
msgstr "Explosion"
#: squirrelbattle/tests/translations_test.py:75
msgid "heart"
msgstr "Herz"
#: squirrelbattle/tests/translations_test.py:69
#: squirrelbattle/tests/translations_test.py:76
msgid "sword"
msgstr "schwert"
#: squirrelbattle/tests/translations_test.py:77
msgid "helmet"
msgstr ""
#: squirrelbattle/tests/translations_test.py:78
msgid "chestplate"
msgstr ""
#: squirrelbattle/tests/translations_test.py:79
msgid "shield"
msgstr ""
#: squirrelbattle/tests/translations_test.py:80
msgid "ring of critical damage"
msgstr ""
#: squirrelbattle/tests/translations_test.py:82
msgid "ring of more experience"
msgstr ""
#: squirrelbattle/tests/translations_test.py:84
msgid "monocle"
msgstr ""

View File

@ -1,49 +1,131 @@
# Spanish translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao
# This file is distributed under the same license as the squirrelbattle package.
# Translation by ifugaao
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+0100\n"
"POT-Creation-Date: 2021-01-08 15:15+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ifugao\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
# Suggested in Weblate: == INVENTORIO ==
#: squirrelbattle/display/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== INVENTORIO =="
msgid "ring_of_critical_damage"
msgstr ""
# Suggested in Weblate: Inventorio :
#: squirrelbattle/display/statsdisplay.py:34
msgid "ring_of_more_experience"
msgstr ""
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} recibe {amount} daño."
#: squirrelbattle/display/creditsdisplay.py:28
#: squirrelbattle/display/menudisplay.py:123
#: squirrelbattle/display/menudisplay.py:148
msgid "Credits"
msgstr ""
#: squirrelbattle/display/creditsdisplay.py:32
msgid "Developers:"
msgstr ""
#: squirrelbattle/display/creditsdisplay.py:38
msgid "Translators:"
msgstr ""
#: squirrelbattle/display/menudisplay.py:168
msgid "INVENTORY"
msgstr "INVENTORIO"
#: squirrelbattle/display/menudisplay.py:214
msgid "STALL"
msgstr "PUESTO"
#: squirrelbattle/display/statsdisplay.py:44
msgid "Inventory:"
msgstr "Inventorio :"
# Suggested in Weblate: ERES MUERTO
#: squirrelbattle/display/statsdisplay.py:50
#: squirrelbattle/display/statsdisplay.py:61
msgid "Equipped main:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:65
msgid "Equipped secondary:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:70
msgid "Equipped chestplate:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:74
msgid "Equipped helmet:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:81
msgid "YOU ARE DEAD"
msgstr "ERES MUERTO"
#: squirrelbattle/display/statsdisplay.py:85
#, python-brace-format
msgid "Use {key} to use the ladder"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:94
msgid "Move to the friendly entity to talk to it"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:96
#, python-brace-format
msgid "Use {key} then move to talk to the entity"
msgstr ""
#: squirrelbattle/entities/friendly.py:33
msgid "I don't sell any squirrel"
msgstr "No vendo ninguna ardilla"
#: squirrelbattle/entities/friendly.py:55
msgid "Flower power!!"
msgstr "Poder de las flores!!"
#: squirrelbattle/entities/friendly.py:55
msgid "The sun is warm today"
msgstr "El sol está caliente hoy"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
#: squirrelbattle/entities/items.py:178
msgid "Bomb is exploding."
msgstr "La bomba está explotando."
#: squirrelbattle/entities/items.py:172
#: squirrelbattle/entities/items.py:365
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} intercambió su cuerpo con {entity}."
#: squirrelbattle/game.py:177
#: squirrelbattle/game.py:200
#, python-brace-format
msgid "The player climbs down to the floor {floor}."
msgstr ""
#: squirrelbattle/game.py:213
#, python-brace-format
msgid "The player climbs up the floor {floor}."
msgstr ""
#: squirrelbattle/game.py:304 squirrelbattle/tests/game_test.py:603
msgid "The buyer does not have enough money"
msgstr "El comprador no tiene suficiente dinero"
#: squirrelbattle/game.py:349
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
@ -51,7 +133,7 @@ msgstr ""
"Algunas claves faltan en su archivo de guarda.\n"
"Su guarda parece a ser corruptido. Fue eliminado."
#: squirrelbattle/game.py:185
#: squirrelbattle/game.py:357
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
@ -59,7 +141,7 @@ msgstr ""
"No jugador encontrado sobre la carta !\n"
"¿ Quizas murió ?"
#: squirrelbattle/game.py:205
#: squirrelbattle/game.py:379
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
@ -67,28 +149,36 @@ msgstr ""
"El JSON archivo no es correcto.\n"
"Su guarda parece corrupta. Fue eliminada."
#: squirrelbattle/interfaces.py:400
#: squirrelbattle/interfaces.py:718
msgid "It's a critical hit!"
msgstr ""
#: squirrelbattle/interfaces.py:719
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} golpea a {opponent}."
#: squirrelbattle/interfaces.py:412
#: squirrelbattle/interfaces.py:733
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} recibe {amount} daño."
msgid "{name} takes {damage} damage."
msgstr ""
#: squirrelbattle/interfaces.py:414
#: squirrelbattle/interfaces.py:735
#, python-brace-format
msgid "{name} dies."
msgstr "{name} se muere."
#: squirrelbattle/menus.py:72
#: squirrelbattle/interfaces.py:769
#, python-brace-format
msgid "{entity} said: {message}"
msgstr "{entity} dijo : {message}"
#: squirrelbattle/menus.py:73
msgid "Back"
msgstr "Volver"
#: squirrelbattle/tests/game_test.py:300,
#: squirrelbattle/tests/game_test.py:303,
#: squirrelbattle/tests/game_test.py:306,
#: squirrelbattle/tests/game_test.py:368 squirrelbattle/tests/game_test.py:371
#: squirrelbattle/tests/game_test.py:374 squirrelbattle/tests/game_test.py:377
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nuevo partido"
@ -166,41 +256,97 @@ msgid "Key used to drop an item in the inventory"
msgstr "Tecla para dejar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:53
msgid "Key used to talk to a friendly entity"
msgstr "Tecla para hablar con una entidad amiga"
#: squirrelbattle/tests/translations_test.py:55
msgid "Key used to wait"
msgstr "Tecla para espera"
#: squirrelbattle/tests/translations_test.py:56
msgid "Key used to use ladders"
msgstr "Tecla para el uso de las escaleras"
#: squirrelbattle/tests/translations_test.py:58
msgid "Texture pack"
msgstr "Paquete de texturas"
#: squirrelbattle/tests/translations_test.py:54
#: squirrelbattle/tests/translations_test.py:59
msgid "Language"
msgstr "Languaje"
#: squirrelbattle/tests/translations_test.py:57
#: squirrelbattle/tests/translations_test.py:62
msgid "player"
msgstr "jugador"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:60
#: squirrelbattle/tests/translations_test.py:64
msgid "hedgehog"
msgstr "erizo"
#: squirrelbattle/tests/translations_test.py:61
#: squirrelbattle/tests/translations_test.py:65
msgid "merchant"
msgstr "comerciante"
#: squirrelbattle/tests/translations_test.py:66
msgid "rabbit"
msgstr "conejo"
#: squirrelbattle/tests/translations_test.py:62
#: squirrelbattle/tests/translations_test.py:67
msgid "sunflower"
msgstr "girasol"
#: squirrelbattle/tests/translations_test.py:68
msgid "teddy bear"
msgstr "osito de peluche"
#: squirrelbattle/tests/translations_test.py:64
#: squirrelbattle/tests/translations_test.py:69
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:70
msgid "eagle"
msgstr ""
#: squirrelbattle/tests/translations_test.py:72
msgid "body snatch potion"
msgstr "poción de intercambio"
#: squirrelbattle/tests/translations_test.py:65
#: squirrelbattle/tests/translations_test.py:73
msgid "bomb"
msgstr "bomba"
#: squirrelbattle/tests/translations_test.py:66
#: squirrelbattle/tests/translations_test.py:74
msgid "explosion"
msgstr "explosión"
#: squirrelbattle/tests/translations_test.py:75
msgid "heart"
msgstr "corazón"
#: squirrelbattle/tests/translations_test.py:76
msgid "sword"
msgstr "espada"
#: squirrelbattle/tests/translations_test.py:77
msgid "helmet"
msgstr ""
#: squirrelbattle/tests/translations_test.py:78
msgid "chestplate"
msgstr ""
#: squirrelbattle/tests/translations_test.py:79
msgid "shield"
msgstr ""
#: squirrelbattle/tests/translations_test.py:80
msgid "ring of critical damage"
msgstr ""
#: squirrelbattle/tests/translations_test.py:82
msgid "ring of more experience"
msgstr ""
#: squirrelbattle/tests/translations_test.py:84
msgid "monocle"
msgstr ""

View File

@ -1,67 +1,126 @@
# French translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse, ifugao
# This file is distributed under the same license as the squirrelbattle package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-11 18:06+0100\n"
"POT-Creation-Date: 2021-01-08 15:15+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: fr\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/menudisplay.py:113
msgid "== INVENTORY =="
msgstr "== INVENTAIRE =="
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
#: squirrelbattle/display/menudisplay.py:134
msgid "== STALL =="
msgstr "== STAND =="
#: squirrelbattle/display/creditsdisplay.py:28
#: squirrelbattle/display/menudisplay.py:123
#: squirrelbattle/display/menudisplay.py:148
msgid "Credits"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:34
#: squirrelbattle/display/creditsdisplay.py:32
msgid "Developers:"
msgstr ""
#: squirrelbattle/display/creditsdisplay.py:38
msgid "Translators:"
msgstr ""
#: squirrelbattle/display/menudisplay.py:168
msgid "INVENTORY"
msgstr "INVENTAIRE"
#: squirrelbattle/display/menudisplay.py:214
msgid "STALL"
msgstr "STAND"
#: squirrelbattle/display/statsdisplay.py:44
msgid "Inventory:"
msgstr "Inventaire :"
#: squirrelbattle/display/statsdisplay.py:53
#: squirrelbattle/display/statsdisplay.py:61
msgid "Equipped main:"
msgstr "Équipement principal :"
#: squirrelbattle/display/statsdisplay.py:65
msgid "Equipped secondary:"
msgstr "Équipement secondaire :"
#: squirrelbattle/display/statsdisplay.py:70
msgid "Equipped chestplate:"
msgstr "Plastron équipé :"
#: squirrelbattle/display/statsdisplay.py:74
msgid "Equipped helmet:"
msgstr "Casque équipé :"
#: squirrelbattle/display/statsdisplay.py:81
msgid "YOU ARE DEAD"
msgstr "VOUS ÊTES MORT"
#: squirrelbattle/display/statsdisplay.py:85
#, python-brace-format
msgid "Use {key} to use the ladder"
msgstr "Appuyez sur {key} pour utiliser l'échelle"
#: squirrelbattle/display/statsdisplay.py:94
msgid "Move to the friendly entity to talk to it"
msgstr "Avancez vers l'entité pour lui parler"
#: squirrelbattle/display/statsdisplay.py:96
#, python-brace-format
msgid "Use {key} then move to talk to the entity"
msgstr "Appuyez sur {key} puis déplacez-vous pour parler"
#. TODO
#: squirrelbattle/entities/friendly.py:33
msgid "I don't sell any squirrel"
msgstr "Je ne vends pas d'écureuil"
#: squirrelbattle/entities/friendly.py:46
#: squirrelbattle/entities/friendly.py:55
msgid "Flower power!!"
msgstr "Pouvoir des fleurs !"
msgstr "Pouvoir des fleurs !!"
#: squirrelbattle/entities/friendly.py:46
#: squirrelbattle/entities/friendly.py:55
msgid "The sun is warm today"
msgstr "Le soleil est chaud aujourd'hui"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:151
#: squirrelbattle/entities/items.py:178
msgid "Bomb is exploding."
msgstr "La bombe explose."
#: squirrelbattle/entities/items.py:224
#: squirrelbattle/entities/items.py:365
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} a échangé son corps avec {entity}."
#: squirrelbattle/game.py:199 squirrelbattle/tests/game_test.py:537
msgid "You do not have enough money"
msgstr ""
#: squirrelbattle/game.py:200
#, python-brace-format
msgid "The player climbs down to the floor {floor}."
msgstr "Le joueur descend à l'étage {floor}."
#: squirrelbattle/game.py:243
#: squirrelbattle/game.py:213
#, python-brace-format
msgid "The player climbs up the floor {floor}."
msgstr "Le joueur monte à l'étage {floor}."
#: squirrelbattle/game.py:304 squirrelbattle/tests/game_test.py:603
msgid "The buyer does not have enough money"
msgstr "L'acheteur n'a pas assez d'argent"
#: squirrelbattle/game.py:349
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
@ -69,7 +128,7 @@ msgstr ""
"Certaines clés de votre ficher de sauvegarde sont manquantes.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/game.py:251
#: squirrelbattle/game.py:357
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
@ -77,7 +136,7 @@ msgstr ""
"Aucun joueur n'a été trouvé sur la carte !\n"
"Peut-être êtes-vous mort ?"
#: squirrelbattle/game.py:271
#: squirrelbattle/game.py:379
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
@ -85,27 +144,36 @@ msgstr ""
"Le fichier JSON de sauvegarde est incorrect.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/interfaces.py:429
#: squirrelbattle/interfaces.py:718
msgid "It's a critical hit!"
msgstr "C'est un coup critique !"
#: squirrelbattle/interfaces.py:719
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} frappe {opponent}."
#: squirrelbattle/interfaces.py:441
#: squirrelbattle/interfaces.py:733
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
msgid "{name} takes {damage} damage."
msgstr "{name} prend {damage} dégâts."
#: squirrelbattle/interfaces.py:443
#: squirrelbattle/interfaces.py:735
#, python-brace-format
msgid "{name} dies."
msgstr "{name} meurt."
#: squirrelbattle/interfaces.py:769
#, python-brace-format
msgid "{entity} said: {message}"
msgstr "{entity} a dit : {message}"
#: squirrelbattle/menus.py:73
msgid "Back"
msgstr "Retour"
#: squirrelbattle/tests/game_test.py:314 squirrelbattle/tests/game_test.py:317
#: squirrelbattle/tests/game_test.py:320
#: squirrelbattle/tests/game_test.py:368 squirrelbattle/tests/game_test.py:371
#: squirrelbattle/tests/game_test.py:374 squirrelbattle/tests/game_test.py:377
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nouvelle partie"
@ -187,45 +255,93 @@ msgid "Key used to talk to a friendly entity"
msgstr "Touche pour parler à une entité pacifique"
#: squirrelbattle/tests/translations_test.py:55
msgid "Key used to wait"
msgstr "Touche pour attendre"
#: squirrelbattle/tests/translations_test.py:56
msgid "Key used to use ladders"
msgstr "Touche pour utiliser les échelles"
#: squirrelbattle/tests/translations_test.py:58
msgid "Texture pack"
msgstr "Pack de textures"
#: squirrelbattle/tests/translations_test.py:56
#: squirrelbattle/tests/translations_test.py:59
msgid "Language"
msgstr "Langue"
#: squirrelbattle/tests/translations_test.py:59
#: squirrelbattle/tests/translations_test.py:62
msgid "player"
msgstr "joueur"
#: squirrelbattle/tests/translations_test.py:61
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:62
#: squirrelbattle/tests/translations_test.py:64
msgid "hedgehog"
msgstr "hérisson"
#: squirrelbattle/tests/translations_test.py:63
#: squirrelbattle/tests/translations_test.py:65
msgid "merchant"
msgstr "marchand"
#: squirrelbattle/tests/translations_test.py:66
msgid "rabbit"
msgstr "lapin"
#: squirrelbattle/tests/translations_test.py:64
#: squirrelbattle/tests/translations_test.py:67
msgid "sunflower"
msgstr "tournesol"
#: squirrelbattle/tests/translations_test.py:68
msgid "teddy bear"
msgstr "nounours"
#: squirrelbattle/tests/translations_test.py:66
#: squirrelbattle/tests/translations_test.py:69
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:70
msgid "eagle"
msgstr "pygargue"
#: squirrelbattle/tests/translations_test.py:72
msgid "body snatch potion"
msgstr "potion d'arrachage de corps"
#: squirrelbattle/tests/translations_test.py:67
#: squirrelbattle/tests/translations_test.py:73
msgid "bomb"
msgstr "bombe"
#: squirrelbattle/tests/translations_test.py:68
#: squirrelbattle/tests/translations_test.py:74
msgid "explosion"
msgstr "explosion"
#: squirrelbattle/tests/translations_test.py:75
msgid "heart"
msgstr "cœur"
#: squirrelbattle/tests/translations_test.py:69
#: squirrelbattle/tests/translations_test.py:76
msgid "sword"
msgstr "épée"
#: squirrelbattle/tests/translations_test.py:77
msgid "helmet"
msgstr "casque"
#: squirrelbattle/tests/translations_test.py:78
msgid "chestplate"
msgstr "plastron"
#: squirrelbattle/tests/translations_test.py:79
msgid "shield"
msgstr "bouclier"
#: squirrelbattle/tests/translations_test.py:80
msgid "ring of critical damage"
msgstr "anneau de coup critique"
#: squirrelbattle/tests/translations_test.py:82
msgid "ring of more experience"
msgstr "anneau de plus d'expérience"
#: squirrelbattle/tests/translations_test.py:84
msgid "monocle"
msgstr "monocle"

View File

@ -14,7 +14,7 @@ from .translations import gettext as _, Translator
class Menu:
"""
A Menu object is the logical representation of a menu in the game
A Menu object is the logical representation of a menu in the game.
"""
values: list
@ -23,26 +23,26 @@ class Menu:
def go_up(self) -> None:
"""
Moves the pointer of the menu on the previous value
Moves the pointer of the menu on the previous value.
"""
self.position = max(0, self.position - 1)
def go_down(self) -> None:
"""
Moves the pointer of the menu on the next value
Moves the pointer of the menu on the next value.
"""
self.position = min(len(self.values) - 1, self.position + 1)
def validate(self) -> Any:
"""
Selects the value that is pointed by the menu pointer
Selects the value that is pointed by the menu pointer.
"""
return self.values[self.position]
class MainMenuValues(Enum):
"""
Values of the main menu
Values of the main menu.
"""
START = "New game"
RESUME = "Resume"
@ -57,14 +57,14 @@ class MainMenuValues(Enum):
class MainMenu(Menu):
"""
A special instance of a menu : the main menu
A special instance of a menu : the main menu.
"""
values = [e for e in MainMenuValues]
class SettingsMenu(Menu):
"""
A special instance of a menu : the settings menu
A special instance of a menu : the settings menu.
"""
waiting_for_key: bool = False
@ -75,7 +75,7 @@ class SettingsMenu(Menu):
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None:
"""
In the setting menu, we van select a setting and change it
In the setting menu, we can select a setting and change it.
"""
if not self.waiting_for_key:
# Navigate normally through the menu.
@ -121,22 +121,40 @@ class SettingsMenu(Menu):
class InventoryMenu(Menu):
"""
A special instance of a menu : the menu for the inventory of the player.
"""
player: Player
def update_player(self, player: Player) -> None:
"""
Updates the player.
"""
self.player = player
@property
def values(self) -> list:
"""
Returns the values of the menu.
"""
return self.player.inventory
class StoreMenu(Menu):
merchant: Merchant
"""
A special instance of a menu : the menu for the inventory of a merchant.
"""
merchant: Merchant = None
def update_merchant(self, merchant: Merchant) -> None:
"""
Updates the merchant.
"""
self.merchant = merchant
@property
def values(self) -> list:
return self.merchant.inventory
"""
Returns the values of the menu.
"""
return self.merchant.inventory if self.merchant else []

View File

@ -13,9 +13,10 @@ from .translations import gettext as _
class Settings:
"""
This class stores the settings of the game.
Settings can be get by using for example settings.TEXTURE_PACK directly.
The comment can be get by using settings.get_comment('TEXTURE_PACK').
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
Settings can be obtained by using for example settings.TEXTURE_PACK
directly.
The comment can be obtained by using settings.get_comment('TEXTURE_PACK').
We can set the setting by simply using settings.TEXTURE_PACK = 'new_key'
"""
def __init__(self):
self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
@ -32,6 +33,8 @@ class Settings:
self.KEY_EQUIP = ['e', 'Key used to equip an item in the inventory']
self.KEY_DROP = ['r', 'Key used to drop an item in the inventory']
self.KEY_CHAT = ['t', 'Key used to talk to a friendly entity']
self.KEY_WAIT = ['w', 'Key used to wait']
self.KEY_LADDER = ['<', 'Key used to use ladders']
self.TEXTURE_PACK = ['ascii', 'Texture pack']
self.LOCALE = [locale.getlocale()[0][:2], 'Language']
@ -49,7 +52,7 @@ class Settings:
def get_comment(self, item: str) -> str:
"""
Retrieve the comment of a setting.
Retrieves the comment relative to a setting.
"""
if item in self.settings_keys:
return _(object.__getattribute__(self, item)[1])
@ -60,21 +63,22 @@ class Settings:
@property
def settings_keys(self) -> Generator[str, Any, None]:
"""
Get the list of all parameters.
Gets the list of all parameters.
"""
return (key for key in self.__dict__)
def loads_from_string(self, json_str: str) -> None:
"""
Dump settings
Loads settings.
"""
d = json.loads(json_str)
for key in d:
setattr(self, key, d[key])
if hasattr(self, key):
setattr(self, key, d[key])
def dumps_to_string(self) -> str:
"""
Dump settings
Dumps settings.
"""
d = dict()
for key in self.settings_keys:

View File

@ -8,7 +8,7 @@ from types import TracebackType
class TermManager: # pragma: no cover
"""
The TermManager object initializes the terminal, returns a screen object and
de-initializes the terminal after use
de-initializes the terminal after use.
"""
def __init__(self):
self.screen = curses.initscr()
@ -21,7 +21,7 @@ class TermManager: # pragma: no cover
# make cursor invisible
curses.curs_set(False)
# Catch mouse events
curses.mousemask(True)
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
# Enable colors
curses.start_color()

View File

@ -1,10 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import random
import unittest
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item, \
Explosion
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit,\
TeddyBear, GiantSeaEagle
from squirrelbattle.entities.friendly import Trumpet
from squirrelbattle.entities.player import Player
from squirrelbattle.interfaces import Entity, Map
from squirrelbattle.resources import ResourceManager
@ -13,16 +16,17 @@ from squirrelbattle.resources import ResourceManager
class TestEntities(unittest.TestCase):
def setUp(self) -> None:
"""
Load example map that can be used in tests.
Loads example map that can be used in tests.
"""
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.player = Player()
self.player.constitution = 0
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
def test_basic_entities(self) -> None:
"""
Test some random stuff with basic entities.
Tests some random stuff with basic entities.
"""
entity = Entity()
entity.move(42, 64)
@ -37,7 +41,7 @@ class TestEntities(unittest.TestCase):
def test_fighting_entities(self) -> None:
"""
Test some random stuff with fighting entities.
Tests some random stuff with fighting entities.
"""
entity = Tiger()
self.map.add_entity(entity)
@ -53,20 +57,21 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.dead)
entity = Rabbit()
entity.critical = 0
self.map.add_entity(entity)
entity.move(15, 44)
# Move randomly
self.map.tick()
self.map.tick(self.player)
self.assertFalse(entity.y == 15 and entity.x == 44)
# Move to the player
entity.move(3, 6)
self.map.tick()
self.map.tick(self.player)
self.assertTrue(entity.y == 2 and entity.x == 6)
# Rabbit should fight
old_health = self.player.health
self.map.tick()
self.map.tick(self.player)
self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health)
self.assertEqual(self.map.logs.messages[-1],
@ -74,6 +79,7 @@ class TestEntities(unittest.TestCase):
{self.player.name.capitalize()} takes {entity.strength} damage.")
# Fight the rabbit
self.player.critical = 0
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
@ -88,9 +94,50 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.dead)
self.assertGreaterEqual(self.player.current_xp, 3)
# Test the familiars
fam = Trumpet()
entity = Rabbit()
self.map.add_entity(entity)
self.map.add_entity(fam)
self.player.move(1, 6)
entity.move(2, 6)
fam.move(2, 7)
# Test fighting
entity.health = 2
entity.paths = []
entity.recalculate_paths()
fam.target = entity
self.map.tick(self.player)
self.assertTrue(entity.dead)
# Test finding a new target
entity2 = Rabbit()
self.map.add_entity(entity2)
entity2.move(2, 6)
self.map.tick(self.player)
self.assertTrue(fam.target == entity2)
self.map.remove_entity(entity2)
# Test following the player and finding the player as target
self.player.move(5, 5)
fam.move(4, 5)
fam.target = None
self.player.move_down()
self.map.tick(self.player)
self.assertTrue(fam.target == self.player)
self.assertEqual(fam.y, 5)
self.assertEqual(fam.x, 5)
# Test random move
fam.move(13, 20)
fam.target = self.player
self.map.tick(self.player)
self.assertTrue(fam.x != 20 or fam.y != 13)
def test_items(self) -> None:
"""
Test some random stuff with items.
Tests some random stuff with items.
"""
item = Item()
self.map.add_entity(item)
@ -111,7 +158,7 @@ class TestEntities(unittest.TestCase):
def test_bombs(self) -> None:
"""
Test some random stuff with bombs.
Tests some random stuff with bombs.
"""
item = Bomb()
hedgehog = Hedgehog()
@ -132,16 +179,30 @@ class TestEntities(unittest.TestCase):
self.assertEqual(item.y, 42)
self.assertEqual(item.x, 42)
# Wait for the explosion
for ignored in range(5):
for _ignored in range(5):
item.act(self.map)
self.assertTrue(hedgehog.dead)
self.assertTrue(teddy_bear.dead)
bomb_state = item.save_state()
self.assertEqual(bomb_state["damage"], item.damage)
explosions = self.map.find_entities(Explosion)
self.assertTrue(explosions)
explosion = explosions[0]
self.assertEqual(explosion.y, item.y)
self.assertEqual(explosion.x, item.x)
# The player can't hold the explosion
explosion.hold(self.player)
self.assertNotIn(explosion, self.player.inventory)
self.assertFalse(explosion.held)
# The explosion disappears after one tick
explosion.act(self.map)
self.assertNotIn(explosion, self.map.entities)
def test_hearts(self) -> None:
"""
Test some random stuff with hearts.
Tests some random stuff with hearts.
"""
item = Heart()
self.map.add_entity(item)
@ -156,7 +217,7 @@ class TestEntities(unittest.TestCase):
def test_body_snatch_potion(self) -> None:
"""
Test some random stuff with body snatch potions.
Tests some random stuff with body snatch potions.
"""
item = BodySnatchPotion()
self.map.add_entity(item)
@ -174,7 +235,7 @@ class TestEntities(unittest.TestCase):
def test_players(self) -> None:
"""
Test some random stuff with players.
Tests some random stuff with players.
"""
player = Player()
self.map.add_entity(player)
@ -204,3 +265,17 @@ class TestEntities(unittest.TestCase):
player_state = player.save_state()
self.assertEqual(player_state["current_xp"], 10)
def test_critical_hit(self) -> None:
"""
Ensure that critical hits are working.
"""
random.seed(2) # Next random.randint(1, 100) will output 8
self.player.critical = 10
sea_eagle = GiantSeaEagle()
self.map.add_entity(sea_eagle)
sea_eagle.move(2, 6)
old_health = sea_eagle.health
self.player.hit(sea_eagle)
self.assertEqual(sea_eagle.health,
old_health - self.player.strength * 4)

View File

@ -1,6 +1,7 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
import os
import unittest
@ -8,11 +9,13 @@ from ..bootstrap import Bootstrap
from ..display.display import Display
from ..display.display_manager import DisplayManager
from ..entities.friendly import Merchant, Sunflower
from ..entities.items import Bomb, Heart, Sword
from ..entities.items import Bomb, Heart, Sword, Explosion, Shield, Helmet, \
Chestplate, RingCritical, Monocle
from ..entities.monsters import GiantSeaEagle
from ..entities.player import Player
from ..enums import DisplayActions
from ..game import Game, KeyValues, GameMode
from ..interfaces import Tile, Map
from ..interfaces import Map, Tile
from ..menus import MainMenuValues
from ..resources import ResourceManager
from ..settings import Settings
@ -22,21 +25,21 @@ from ..translations import gettext as _, Translator
class TestGame(unittest.TestCase):
def setUp(self) -> None:
"""
Setup game.
Sets the game up.
"""
self.game = Game()
self.game.new_game()
self.game.map = Map.load(ResourceManager.get_asset_path(
"example_map.txt"))
self.game.player.move(self.game.map.start_y, self.game.map.start_x)
self.game.map = Map.load(
ResourceManager.get_asset_path("example_map.txt"))
self.game.map.add_entity(self.game.player)
self.game.player.move(self.game.map.start_y, self.game.map.start_x)
self.game.logs.add_message("Hello World !")
display = DisplayManager(None, self.game)
self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None:
"""
Save a game and reload it.
Saves a game and reloads it.
"""
bomb = Bomb()
self.game.map.add_entity(bomb)
@ -63,6 +66,7 @@ class TestGame(unittest.TestCase):
new_state = self.game.save_state()
self.assertEqual(old_state, new_state)
self.assertIsNone(self.game.message)
# Ensure that the bomb is loaded
self.assertTrue(self.game.player.inventory)
@ -90,7 +94,7 @@ class TestGame(unittest.TestCase):
def test_bootstrap_fail(self) -> None:
"""
Ensure that the test can't play the game,
Ensures that the test can't play the game,
because there is no associated shell.
Yeah, that's only for coverage.
"""
@ -99,7 +103,7 @@ class TestGame(unittest.TestCase):
def test_key_translation(self) -> None:
"""
Test key bindings.
Tests key bindings.
"""
self.game.settings = Settings()
@ -145,6 +149,12 @@ class TestGame(unittest.TestCase):
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DROP, self.game.settings),
KeyValues.DROP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_WAIT, self.game.settings),
KeyValues.WAIT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LADDER, self.game.settings),
KeyValues.LADDER)
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
KeyValues.SPACE)
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
@ -152,7 +162,7 @@ class TestGame(unittest.TestCase):
def test_key_press(self) -> None:
"""
Press a key and see what is done.
Presses a key and asserts what is done is correct.
"""
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.assertEqual(self.game.main_menu.validate(),
@ -238,17 +248,28 @@ class TestGame(unittest.TestCase):
self.assertEqual(new_y, y)
self.assertEqual(new_x, x - 1)
explosion = Explosion()
self.game.map.add_entity(explosion)
self.assertIn(explosion, self.game.map.entities)
self.game.handle_key_pressed(KeyValues.WAIT)
self.assertNotIn(explosion, self.game.map.entities)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_mouse_click(self) -> None:
"""
Simulate mouse clicks.
Simulates mouse clicks.
"""
self.game.state = GameMode.MAINMENU
# Change the color of the artwork
self.game.display_actions(DisplayActions.MOUSE, 0, 10,
curses.BUTTON1_CLICKED)
# Settings menu
self.game.display_actions(DisplayActions.MOUSE, 25, 21)
self.game.display_actions(DisplayActions.MOUSE, 25, 21,
curses.BUTTON1_CLICKED)
self.assertEqual(self.game.main_menu.position, 4)
self.assertEqual(self.game.state, GameMode.SETTINGS)
@ -260,17 +281,19 @@ class TestGame(unittest.TestCase):
self.game.state = GameMode.INVENTORY
# Click nowhere
self.game.display_actions(DisplayActions.MOUSE, 0, 0)
self.game.display_actions(DisplayActions.MOUSE, 0, 0,
curses.BUTTON1_CLICKED)
self.assertEqual(self.game.state, GameMode.INVENTORY)
# Click on the second item
self.game.display_actions(DisplayActions.MOUSE, 8, 25)
self.game.display_actions(DisplayActions.MOUSE, 8, 25,
curses.BUTTON1_CLICKED)
self.assertEqual(self.game.state, GameMode.INVENTORY)
self.assertEqual(self.game.inventory_menu.position, 1)
def test_new_game(self) -> None:
"""
Ensure that the start button starts a new game.
Ensures that the start button starts a new game.
"""
old_map = self.game.map
old_player = self.game.player
@ -293,7 +316,7 @@ class TestGame(unittest.TestCase):
def test_settings_menu(self) -> None:
"""
Ensure that the settings menu is working properly.
Ensures that the settings menu is working properly.
"""
self.game.settings = Settings()
@ -305,13 +328,13 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
# Define the "move up" key to 'w'
# Define the "move up" key to 'h'
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'w')
self.game.handle_key_pressed(None, 'h')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'h')
# Navigate to "move left"
self.game.handle_key_pressed(KeyValues.DOWN)
@ -332,7 +355,7 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack"
for ignored in range(10):
for ignored in range(12):
self.game.handle_key_pressed(KeyValues.DOWN)
# Change texture pack
@ -379,7 +402,7 @@ class TestGame(unittest.TestCase):
def test_dead_screen(self) -> None:
"""
Kill player and render dead screen.
Kills the player and renders the dead message on the fake screen.
"""
self.game.state = GameMode.PLAY
# Kill player
@ -395,13 +418,14 @@ class TestGame(unittest.TestCase):
def test_not_implemented(self) -> None:
"""
Check that some functions are not implemented, only for coverage.
Checks that some functions are not implemented, only for coverage.
"""
self.assertRaises(NotImplementedError, Display.display, None)
self.assertRaises(NotImplementedError, Display.update, None, self.game)
def test_messages(self) -> None:
"""
Display error messages.
Displays error messages.
"""
self.game.message = "I am an error"
self.game.display_actions(DisplayActions.UPDATE)
@ -411,7 +435,7 @@ class TestGame(unittest.TestCase):
def test_inventory_menu(self) -> None:
"""
Open the inventory menu and interact with items.
Opens the inventory menu and interacts with items.
"""
self.game.state = GameMode.PLAY
# Open and close the inventory
@ -472,7 +496,7 @@ class TestGame(unittest.TestCase):
def test_talk_to_sunflowers(self) -> None:
"""
Interact with sunflowers
Interacts with sunflowers.
"""
self.game.state = GameMode.PLAY
@ -504,8 +528,8 @@ class TestGame(unittest.TestCase):
self.assertEqual(self.game.state, GameMode.PLAY)
self.assertTrue(self.game.logs.messages)
# Ensure that the message is a good message
self.assertIn(self.game.logs.messages[1][21:],
Sunflower.dialogue_option)
self.assertTrue(any(self.game.logs.messages[1].endswith(msg)
for msg in Sunflower().dialogue_option))
# Test all directions to detect the friendly entity
self.game.player.move(sunflower.y + 1, sunflower.x)
@ -523,7 +547,7 @@ class TestGame(unittest.TestCase):
def test_talk_to_merchant(self) -> None:
"""
Interact with merchants
Interacts with merchants.
"""
self.game.state = GameMode.PLAY
@ -545,21 +569,28 @@ class TestGame(unittest.TestCase):
# Navigate in the menu
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.LEFT)
self.assertFalse(self.game.is_in_store_menu)
self.game.handle_key_pressed(KeyValues.RIGHT)
self.assertTrue(self.game.is_in_store_menu)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.store_menu.position, 1)
self.game.player.hazel = 0x7ffff42ff
# The second item is not a heart
merchant.inventory[1] = Sword()
merchant.inventory[1] = sword = Sword()
# Buy the second item by clicking on it
item = self.game.store_menu.validate()
self.assertIn(item, merchant.inventory)
self.game.display_actions(DisplayActions.MOUSE, 8, 25)
self.game.handle_key_pressed(KeyValues.ENTER)
self.game.display_actions(DisplayActions.MOUSE, 7, 25,
curses.BUTTON1_CLICKED)
self.assertIn(item, self.game.player.inventory)
self.assertNotIn(item, merchant.inventory)
# Buy a heart
merchant.inventory[1] = Heart()
self.game.display_actions(DisplayActions.REFRESH)
item = self.game.store_menu.validate()
self.assertIn(item, merchant.inventory)
self.assertEqual(item, merchant.inventory[1])
@ -576,9 +607,169 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertNotIn(item, self.game.player.inventory)
self.assertIn(item, merchant.inventory)
self.assertEqual(self.game.message, _("You do not have enough money"))
self.assertEqual(self.game.message,
_("The buyer does not have enough money"))
self.game.handle_key_pressed(KeyValues.ENTER)
# Sell an item
self.game.inventory_menu.position = len(self.game.player.inventory) - 1
self.game.handle_key_pressed(KeyValues.LEFT)
self.assertFalse(self.game.is_in_store_menu)
self.assertIn(sword, self.game.player.inventory)
self.assertEqual(self.game.inventory_menu.validate(), sword)
old_player_money, old_merchant_money = self.game.player.hazel,\
merchant.hazel
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertNotIn(sword, self.game.player.inventory)
self.assertIn(sword, merchant.inventory)
self.assertEqual(self.game.player.hazel, old_player_money + sword.price)
self.assertEqual(merchant.hazel, old_merchant_money - sword.price)
# Exit the menu
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY)
def test_equipment(self) -> None:
"""
Ensure that equipment is working.
"""
self.game.state = GameMode.INVENTORY
# sword goes into the main equipment slot
sword = Sword()
sword.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_main, sword)
self.assertFalse(self.game.player.inventory)
# shield goes into the secondary equipment slot
shield = Shield()
shield.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_secondary, shield)
self.assertFalse(self.game.player.inventory)
# helmet goes into the helmet slot
helmet = Helmet()
helmet.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_helmet, helmet)
self.assertFalse(self.game.player.inventory)
# helmet goes into the armor slot
chestplate = Chestplate()
chestplate.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_armor, chestplate)
self.assertFalse(self.game.player.inventory)
# Use bomb
bomb = Bomb()
bomb.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_secondary, bomb)
self.assertIn(shield, self.game.player.inventory)
self.game.state = GameMode.PLAY
self.game.handle_key_pressed(KeyValues.USE)
self.assertIsNone(self.game.player.equipped_secondary)
self.game.state = GameMode.INVENTORY
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.equipped_secondary, shield)
self.assertFalse(self.game.player.inventory)
# Reequip, which is useless but covers code
sword.equip()
shield.equip()
helmet.equip()
chestplate.equip()
self.game.save_state()
# Unequip all
sword.unequip()
shield.unequip()
helmet.unequip()
chestplate.unequip()
self.assertIsNone(self.game.player.equipped_main)
self.assertIsNone(self.game.player.equipped_secondary)
self.assertIsNone(self.game.player.equipped_helmet)
self.assertIsNone(self.game.player.equipped_armor)
self.assertIn(sword, self.game.player.inventory)
self.assertIn(shield, self.game.player.inventory)
self.assertIn(helmet, self.game.player.inventory)
self.assertIn(chestplate, self.game.player.inventory)
# Test rings
self.game.player.inventory.clear()
ring = RingCritical()
ring.hold(self.game.player)
old_critical = self.game.player.critical
self.game.handle_key_pressed(KeyValues.EQUIP)
self.assertEqual(self.game.player.critical,
old_critical + ring.critical)
self.game.save_state()
ring.unequip()
def test_monocle(self) -> None:
"""
The player is wearing a monocle, then the stats are displayed.
"""
self.game.state = GameMode.PLAY
monocle = Monocle()
monocle.hold(self.game.player)
monocle.equip()
sea_eagle = GiantSeaEagle()
self.game.map.add_entity(sea_eagle)
sea_eagle.move(2, 6)
self.game.display_actions(DisplayActions.REFRESH)
def test_ladders(self) -> None:
"""
Ensure that the player can climb on ladders.
"""
self.game.state = GameMode.PLAY
self.assertEqual(self.game.player.map.floor, 0)
self.game.handle_key_pressed(KeyValues.LADDER)
self.assertEqual(self.game.player.map.floor, 0)
# Move nowhere
self.game.player.move(10, 10)
self.game.handle_key_pressed(KeyValues.LADDER)
self.assertEqual(self.game.player.map.floor, 0)
# Move down
self.game.player.move(3, 40) # Move on a ladder
self.game.handle_key_pressed(KeyValues.LADDER)
self.assertEqual(self.game.map_index, 1)
self.assertEqual(self.game.player.map.floor, 1)
self.assertEqual(self.game.player.y, 1)
self.assertEqual(self.game.player.x, 17)
self.game.display_actions(DisplayActions.UPDATE)
# Move up
self.game.handle_key_pressed(KeyValues.LADDER)
self.assertEqual(self.game.player.map.floor, 0)
self.assertEqual(self.game.player.y, 3)
self.assertEqual(self.game.player.x, 40)
self.game.display_actions(DisplayActions.UPDATE)
def test_credits(self) -> None:
"""
Load credits menu.
"""
self.game.state = GameMode.MAINMENU
self.game.display_actions(DisplayActions.MOUSE, 41, 41,
curses.BUTTON1_CLICKED)
self.assertEqual(self.game.state, GameMode.CREDITS)
self.game.display_actions(DisplayActions.MOUSE, 21, 21,
curses.BUTTON1_CLICKED)
self.game.display_actions(DisplayActions.REFRESH)
self.game.state = GameMode.CREDITS
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)

View File

@ -4,14 +4,14 @@
import unittest
from squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.interfaces import Map, Tile
from squirrelbattle.interfaces import Map, Tile, Slope
from squirrelbattle.resources import ResourceManager
class TestInterfaces(unittest.TestCase):
def test_map(self) -> None:
"""
Create a map and check that it is well parsed.
Creates a map and checks that it is well parsed.
"""
m = Map.load_from_string("0 0\n.#\n#.\n")
self.assertEqual(m.width, 2)
@ -20,7 +20,7 @@ class TestInterfaces(unittest.TestCase):
def test_load_map(self) -> None:
"""
Try to load a map from a file.
Tries to load a map from a file.
"""
m = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.assertEqual(m.width, 52)
@ -28,7 +28,7 @@ class TestInterfaces(unittest.TestCase):
def test_tiles(self) -> None:
"""
Test some things about tiles.
Tests some things about tiles.
"""
self.assertFalse(Tile.FLOOR.is_wall())
self.assertTrue(Tile.WALL.is_wall())
@ -37,3 +37,21 @@ class TestInterfaces(unittest.TestCase):
self.assertFalse(Tile.WALL.can_walk())
self.assertFalse(Tile.EMPTY.can_walk())
self.assertRaises(ValueError, Tile.from_ascii_char, 'unknown')
def test_slope(self) -> None:
"""
Test good behaviour of slopes (basically vectors, compared according to
the determinant)
"""
a = Slope(1, 1)
b = Slope(0, 1)
self.assertTrue(b < a)
self.assertTrue(b <= a)
self.assertTrue(a <= a)
self.assertTrue(a == a)
self.assertTrue(a > b)
self.assertTrue(a >= b)
# def test_visibility(self) -> None:
# m = Map.load(ResourceManager.get_asset_path("example_map_3.txt"))
# m.compute_visibility(1, 1, 50)

View File

@ -12,8 +12,8 @@ class FakePad:
def addstr(self, y: int, x: int, message: str, color: int = 0) -> None:
pass
def refresh(self, pminrow: int, pmincol: int, sminrow: int,
smincol: int, smaxrow: int, smaxcol: int) -> None:
def noutrefresh(self, pminrow: int, pmincol: int, sminrow: int,
smincol: int, smaxrow: int, smaxcol: int) -> None:
pass
def erase(self) -> None:
@ -24,3 +24,6 @@ class FakePad:
def getmaxyx(self) -> Tuple[int, int]:
return 42, 42
def inch(self, y: int, x: int) -> str:
return "i"

View File

@ -13,7 +13,7 @@ class TestSettings(unittest.TestCase):
def test_settings(self) -> None:
"""
Ensure that settings are well loaded.
Ensures that settings are well loaded.
"""
settings = Settings()
self.assertEqual(settings.KEY_UP_PRIMARY, 'z')

View File

@ -11,7 +11,7 @@ class TestTranslations(unittest.TestCase):
def test_main_menu_translation(self) -> None:
"""
Ensure that the main menu is translated.
Ensures that the main menu is translated.
"""
self.assertEqual(_("New game"), "Nouvelle partie")
self.assertEqual(_("Resume"), "Continuer")
@ -22,7 +22,7 @@ class TestTranslations(unittest.TestCase):
def test_settings_menu_translation(self) -> None:
"""
Ensure that the settings menu is translated.
Ensures that the settings menu is translated.
"""
self.assertEqual(_("Main key to move up"),
"Touche principale pour aller vers le haut")
@ -52,18 +52,33 @@ class TestTranslations(unittest.TestCase):
"Touche pour jeter un objet de l'inventaire")
self.assertEqual(_("Key used to talk to a friendly entity"),
"Touche pour parler à une entité pacifique")
self.assertEqual(_("Key used to wait"), "Touche pour attendre")
self.assertEqual(_("Key used to use ladders"),
"Touche pour utiliser les échelles")
self.assertEqual(_("Texture pack"), "Pack de textures")
self.assertEqual(_("Language"), "Langue")
def test_entities_translation(self) -> None:
self.assertEqual(_("player"), "joueur")
self.assertEqual(_("tiger"), "tigre")
self.assertEqual(_("hedgehog"), "hérisson")
self.assertEqual(_("merchant"), "marchand")
self.assertEqual(_("rabbit"), "lapin")
self.assertEqual(_("sunflower"), "tournesol")
self.assertEqual(_("teddy bear"), "nounours")
self.assertEqual(_("tiger"), "tigre")
self.assertEqual(_("eagle"), "pygargue")
self.assertEqual(_("body snatch potion"), "potion d'arrachage de corps")
self.assertEqual(_("bomb"), "bombe")
self.assertEqual(_("explosion"), "explosion")
self.assertEqual(_("heart"), "cœur")
self.assertEqual(_("sword"), "épée")
self.assertEqual(_("helmet"), "casque")
self.assertEqual(_("chestplate"), "plastron")
self.assertEqual(_("shield"), "bouclier")
self.assertEqual(_("ring of critical damage"),
"anneau de coup critique")
self.assertEqual(_("ring of more experience"),
"anneau de plus d'expérience")
self.assertEqual(_("monocle"), "monocle")

View File

@ -13,7 +13,7 @@ class Translator:
"""
This module uses gettext to translate strings.
Translator.setlocale defines the language of the strings,
then gettext() translates the message.
then gettext() translates the messages.
"""
SUPPORTED_LOCALES: List[str] = ["de", "en", "es", "fr"]
locale: str = "en"
@ -22,7 +22,7 @@ class Translator:
@classmethod
def refresh_translations(cls) -> None:
"""
Load compiled translations.
Loads compiled translations.
"""
for language in cls.SUPPORTED_LOCALES:
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
@ -37,7 +37,7 @@ class Translator:
@classmethod
def setlocale(cls, lang: str) -> None:
"""
Define the language used to translate the game.
Defines the language used to translate the game.
The language must be supported, otherwise nothing is done.
"""
lang = lang[:2]
@ -51,7 +51,7 @@ class Translator:
@classmethod
def makemessages(cls) -> None: # pragma: no cover
"""
Analyse all strings in the project and extract them.
Analyses all strings in the project and extracts them.
"""
for language in cls.SUPPORTED_LOCALES:
if language == "en":
@ -83,7 +83,7 @@ class Translator:
@classmethod
def compilemessages(cls) -> None:
"""
Compile translation messages from source files.
Compiles translation messages from source files.
"""
for language in cls.SUPPORTED_LOCALES:
if language == "en":
@ -99,7 +99,7 @@ class Translator:
def gettext(message: str) -> str:
"""
Translate a message.
Translates a message.
"""
return Translator.get_translator().gettext(message)