Merge remote-tracking branch 'origin/master' into village

# Conflicts:
#	squirrelbattle/display/texturepack.py
#	squirrelbattle/interfaces.py
This commit is contained in:
Yohann D'ANELLO 2020-12-01 17:07:40 +01:00
commit 29142cd91c
52 changed files with 1763 additions and 206 deletions

3
.gitignore vendored
View File

@ -23,3 +23,6 @@ save.json
# Don't commit docs output # Don't commit docs output
docs/_build docs/_build
# Don't commit compiled messages
*.mo

View File

@ -7,6 +7,7 @@ py37:
stage: test stage: test
image: python:3.7-alpine image: python:3.7-alpine
before_script: before_script:
- apk add --no-cache gettext
- pip install tox - pip install tox
script: tox -e py3 script: tox -e py3
@ -14,6 +15,7 @@ py38:
stage: test stage: test
image: python:3.8-alpine image: python:3.8-alpine
before_script: before_script:
- apk add --no-cache gettext
- pip install tox - pip install tox
script: tox -e py3 script: tox -e py3
@ -22,6 +24,7 @@ py39:
stage: test stage: test
image: python:3.9-alpine image: python:3.9-alpine
before_script: before_script:
- apk add --no-cache gettext
- pip install tox - pip install tox
script: tox -e py3 script: tox -e py3
@ -37,7 +40,7 @@ build-deb:
image: debian:buster-slim image: debian:buster-slim
stage: build stage: build
before_script: before_script:
- apt-get update && apt-get -y --no-install-recommends install build-essential debmake dh-python debhelper 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: script:
- dpkg-buildpackage - dpkg-buildpackage
- mkdir build && cp ../*.deb build/ - mkdir build && cp ../*.deb build/
@ -45,3 +48,5 @@ build-deb:
paths: paths:
- build/*.deb - build/*.deb
expire_in: 1 week expire_in: 1 week
only:
- master

View File

@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
Squirrel Battle Squirrel Battle
Copyright (C) 2020 ynerant Copyright (C) 2020 ÿnérant, eichhornchen, nicomarg, charlse
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
Squirrel Battle Copyright (C) 2020 ynerant Squirrel Battle Copyright (C) 2020 ÿnérant, eichhornchen, nicomarg, charlse
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

View File

@ -1,7 +1,7 @@
[![pipeline status](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/pipeline.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master) [![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) [![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) [![Documentation Status](https://readthedocs.org/projects/squirrel-battle/badge/?version=latest)](https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest)
[![PyPI](https://img.shields.io/pypi/v/dungeon-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/) [![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/) [![AUR version](https://img.shields.io/aur/version/python-squirrel-battle)](https://aur.archlinux.org/packages/python-squirrel-battle/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)

View File

@ -2,4 +2,4 @@ Squirrel Battle
Watch out for squirrel's knifes! Watch out for squirrel's knifes!
-- Yohann D'ANELLO <ynerant@crans.org> Thu, 19 Nov 2020 03:30:42 +0100 -- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 19 Nov 2020 03:30:42 +0100

10
debian/changelog vendored
View File

@ -1,5 +1,11 @@
python3-squirrel-battle (3.14) beta; urgency=low python3-squirrel-battle (3.14.1) beta; urgency=low
* Some graphical improvements.
-- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 27 Nov 2020 18:25:42 +0100
python3-squirrel-battle (3.14) beta; urgency=low
* Initial release. * Initial release.
-- Yohann D'ANELLO <ynerant@crans.org> Thu, 19 Nov 2020 03:30:42 +0100 -- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 19 Nov 2020 03:30:42 +0100

4
debian/control vendored
View File

@ -1,8 +1,8 @@
Source: python3-squirrel-battle Source: python3-squirrel-battle
Section: devel Section: devel
Priority: optional Priority: optional
Maintainer: ynerant <ynrant@crans.org> Maintainer: ynerant <squirrel-battle@crans.org>
Build-Depends: debhelper (>=10~), dh-python, python3-all, python3-setuptools Build-Depends: debhelper (>=10~), dh-python, gettext, python3-all, python3-setuptools
Depends: fonts-noto-color-emoji Depends: fonts-noto-color-emoji
Standards-Version: 4.1.4 Standards-Version: 4.1.4
Homepage: https://gitlab.crans.org/ynerant/squirrel-battle Homepage: https://gitlab.crans.org/ynerant/squirrel-battle

6
debian/copyright vendored
View File

@ -1,11 +1,11 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Yohann D'ANELLO Upstream-Name: ÿnérant, eichhornchen, nicomarg, charlse
Upstream-Contact: Yohann D'ANELLO <ynerant@crans.org> Upstream-Contact: ÿnérant, eichhornchen, nicomarg, charlse <squirrel-battle@crans.org>
Source: https://gitlab.crans.org/ynerant/squirrel-battle Source: https://gitlab.crans.org/ynerant/squirrel-battle
Files: * Files: *
Copyright: 2020 Yohann D'ANELLO <ynerant@crans.org> Copyright: 2020 ÿnérant, eichhornchen, nicomarg, charlse <squirrel-battle@crans.org>
License: GPL-3+ License: GPL-3+
This program is free software; you can redistribute it This program is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public and/or modify it under the terms of the GNU General Public

323
docs/deployment.rst Normal file
View File

@ -0,0 +1,323 @@
Déploiement du projet
=====================
.. _PyPI: https://pypi.org/project/squirrel-battle/
.. _AUR: https://aur.archlinux.org/packages/python-squirrel-battle/
.. _Debian: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14.1_all.deb?job=build-deb
.. _installation: install.html
À chaque nouvelle version du projet, il est compilé et déployé dans PyPI_, dans
l'AUR_ et un paquet Debian_ est créé, voir la page d'installation_.
PyPI
----
Définition du paquet
~~~~~~~~~~~~~~~~~~~~
.. _setup.py: https://gitlab.crans.org/ynerant/squirrel-battle/-/blob/master/setup.py
La documentation sur le packaging dans PyPI_ est disponible `ici
<https://packaging.python.org/tutorials/packaging-projects/>`_.
Le fichier `setup.py`_ contient l'ensemble des instructions d'installation du
paquet ainsi que des détails à fournir à PyPI :
.. code:: python
#!/usr/bin/env python3
import os
from setuptools import find_packages, setup
with open("README.md", "r") as f:
long_description = f.read()
# Compile messages
for language in ["de", "en", "fr"]:
args = ["msgfmt", "--check-format",
"-o", f"squirrelbattle/locale/{language}/LC_MESSAGES"
"/squirrelbattle.mo",
f"squirrelbattle/locale/{language}/LC_MESSAGES"
"/squirrelbattle.po"]
print(f"Compiling {language} messages...")
subprocess.Popen(args)
setup(
name="squirrel-battle",
version="3.14.1",
author="ÿnérant, eichhornchen, nicomarg, charlse",
author_email="squirrel-battle@crans.org",
description="Watch out for squirrel's knives!",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://gitlab.crans.org/ynerant/squirrel-battle",
packages=find_packages(),
license='GPLv3',
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console :: Curses",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Natural Language :: French",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Topic :: Games/Entertainment",
],
python_requires='>=3.6',
include_package_data=True,
package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]},
entry_points={
"console_scripts": [
"squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game",
]
}
)
Ce fichier contient le nom du paquet, sa version, l'auteur et son contact,
sa description en une ligne et sa description longue, le lien d'accueil du projet,
sa licence, ses classificateurs et son exécutable.
Il commence tout d'abord par compiler les fichiers de `traduction <translation.html>`_.
Le paramètre ``entry_points`` définit un exécutable nommé ``squirrel-battle``,
qui permet de lancer le jeu.
Installation locale du paquet
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
L'installation du paquet localement dans son environnement Python (virtuel ou non)
peut se faire en exécutant ``pip install -e .``.
Génération des binaires
~~~~~~~~~~~~~~~~~~~~~~~
Les paquets ``setuptools`` (``python3-setuptools`` pour APT, ``python-setuptools``
pour pacman) et ``wheel`` (``python3-wheel`` pour APT, ``python-wheel`` pour pacman)
sont nécessaires. Une fois installés, il faut appeler la commande :
.. code:: bash
python3 setup.py sdist bdist_wheel
Une fois cela fait, le dossier ``dist/`` contiendra les archives à transmettre à PyPI.
Publier sur PyPI
~~~~~~~~~~~~~~~~
Il faut avant tout avoir un compte sur PyPI. Dans `votre compte PyPI
<https://pypi.org/manage/account/>`_, il faut générer un jeton d'accès API.
Dans le fichier ``.pypirc`` dans le répertoire principal de l'utilisateur,
il faut ajouter le jeton d'accès :
.. code::
[pypi]
username = __token__
password = pypi-my-pypi-api-access-token
Cela permet de s'authentifier directement par ce jeton.
Ensuite, il faut installer ``twine``, qui permet de publier sur PyPI.
Il suffit ensuite d'appeler :
.. code:: bash
twine upload dist/*
pour envoyer le paquet sur PyPI.
.. note::
À des fins de tests, il est possible d'utiliser le dépôt `<https://test.pypi.org>`_.
Les différences sont au niveau de l'authentification, où il faut l'en-tête
``[testpypi]`` dans le ``.pypirc``, et il faut envoyer le paquet avec
``twine upload --repository testpypi dist/``.
Publier dans l'AUR
------------------
Fonctionnement du packaging
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. _python-squirrel-battle: https://aur.archlinux.org/packages/python-squirrel-battle/
.. _python-squirrel-battle-git: https://aur.archlinux.org/packages/python-squirrel-battle-git/
Deux paquets sont publiés dans l'AUR (Arch User Repository) :
- python-squirrel-battle_
- python-squirrel-battle-git_
Le packaging dans Arch Linux se fait en commitant un fichier ``PKGBUILD`` dans
le dépôt à l'adresse ``ssh://aur@aur.archlinux.org/packagename.git``,
en remplaçant ``packagename`` par le nom du paquet.
Le second paquet compile directement le jeu à partir de la branche ``master``
du dépôt Git. Le fichier ``PKGBUILD`` dispose de cette structure :
.. code::
# Maintainer: Yohann D'ANELLO <squirrel-battle@crans.org>
pkgbase=squirrel-battle
pkgname=python-squirrel-battle-git
pkgver=3.14.1
pkgrel=1
pkgdesc="Watch out for squirrel's knives!"
arch=('any')
url="https://gitlab.crans.org/ynerant/squirrel-battle"
license=('GPLv3')
depends=('python')
makedepends=('gettext' 'python-setuptools')
depends=('noto-fonts-emoji')
checkdepends=('python-tox')
ssource=("git+https://gitlab.crans.org/ynerant/squirrel-battle.git")
sha256sums=("SKIP")
pkgver() {
cd pkgbase
git describe --long --tags | sed -r 's/^v//;s/([^-]*-g)/r\1/;s/-/./g'
}
build() {
cd $pkgbase
python setup.py build
}
check() {
cd $pkgbase
tox -e py3
tox -e linters
}
package() {
cd $pkgbase
python setup.py install --skip-build \
--optimize=1 \
--root="${pkgdir}"
install -vDm 644 README.md \
-t "${pkgdir}/usr/share/doc/${pkgname}"
install -vDm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}"
}
Ces instructions permettent de cloner le dépôt, l'installer et exécuter des tests,
en plus de définir les attributs du paquet.
Le fichier ``PKGBUILD`` du paquet ``python-squirrel-battle``, synchronisé avec
les releases, est plus ou moins similaire :
.. code::
# Maintainer: Yohann D'ANELLO <squirrel-battle@crans.org>
pkgbase=squirrel-battle
pkgname=python-squirrel-battle
pkgver=3.14.1
pkgrel=1
pkgdesc="Watch out for squirrel's knives!"
arch=('any')
url="https://gitlab.crans.org/ynerant/squirrel-battle"
license=('GPLv3')
depends=('python')
makedepends=('gettext' 'python-setuptools')
depends=('noto-fonts-emoji')
checkdepends=('python-tox')
source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v3.14.1/$pkgbase-v$pkgver.tar.gz")
sha256sums=("6090534d598c0b3a8f5acdb553c12908ba8107d62d08e17747d1dbb397bddef0")
build() {
cd $pkgbase-v$pkgver
python setup.py build
}
check() {
cd $pkgbase-v$pkgver
tox -e py3
tox -e linters
}
package() {
cd $pkgbase-v$pkgver
python setup.py install --skip-build \
--optimize=1 \
--root="${pkgdir}"
install -vDm 644 README.md \
-t "${pkgdir}/usr/share/doc/${pkgname}"
install -vDm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}"
}
Il se contente ici de télécharger l'archive de la dernière release, et de travailler
dessus.
Mettre à jour
~~~~~~~~~~~~~
Pour mettre à jour le dépôt, une fois les dépôts
``ssh://aur@aur.archlinux.org/python-squirrel-battle.git`` et
``ssh://aur@aur.archlinux.org/python-squirrel-battle-git.git`` clonés,
il suffit de mettre à jour le paramètre ``pkgver`` pour la bonne version,
de régénérer le fichier ``.SRCINFO`` en faisant
``makepkg --printsrcinfo > .SRCINFO``, puis de committer/pousser.
Construction du paquet Debian
-----------------------------
Structure du paquet
-------------------
L'ensemble des instructions pour construire le paquet Debian est situé dans le
dossier ``debian/``.
Le fichier ``changelog`` est à modifier à chaque nouvelle version, le fichier
``compat`` contient la version minimale de Debian requise (``10`` pour Debian
Buster), le fichier ``copyright`` contient la liste des fichiers distribués sous
quelle licence (ici GPLv3), le fichier ``control`` contient les informations du
paquet, le fichier ``install`` les fichiers de configuration à installer
(ici le fix de l'affichage de l'écurueil), et enfin le fichier ``rules`` l'ensemble
des instructions à exécuter pour installer.
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.
D'abord on installe les paquets nécessaires :
.. code::
apt update
apt --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools
On peut ensuite construire le paquet :
.. code:: bash
dpkg-buildpackage
mkdir build && cp ../*.deb build/
Le paquet sera installé dans ``build/python3-squirrel-battle_3.14.1_all.deb``.
Le paquet Debian_ est construit par l'intégration continue Gitlab et ajouté
à chaque release.

31
docs/documentation.rst Normal file
View File

@ -0,0 +1,31 @@
Documentation
=============
La documentation est gérée grâce à Sphinx. Le thème est le thème officiel de
ReadTheDocs ``sphinx-rtd-theme``.
Générer localement la documentation
-----------------------------------
On commence par se rendre au bon endroit et installer les bonnes dépendances :
.. code:: bash
cd docs
pip install -r requirements.txt
La documentation se génère à partir d'appels à ``make``, selon le type de
documentation voulue.
Par exemple, ``make html`` construit la documentation web, ``make latexpdf``
construit un livre PDF avec cette documentation.
Documentation externe
---------------------
À chaque commit, un webhook est envoyé à `<readthedocs.io>`_, qui construit
tout seul la documentation Sphinx, la publiant à l'adresse
`<squirrel-battle.readthedocs.io>`_.
De plus, les documentations sont sauvegardées à chaque release taguée.

View File

@ -25,14 +25,14 @@ 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 ``🦔``. Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦔``.
Castor Tigre
------ -----
Son nom est fixé à `beaver`. Il a par défaut une force à **2** et **20** points de vie. Son nom est fixé à `tiger`. Il a par défaut une force à **2** et **20** points de vie.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``_``. Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``n``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦫``. Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐅``.
Lapin Lapin

View File

@ -13,11 +13,11 @@ Bienvenue dans la documentation de Squirrel Battle !
:target: https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest :target: https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest
:alt: Documentation Status :alt: Documentation Status
.. image:: https://img.shields.io/pypi/v/dungeon-battle .. image:: https://img.shields.io/pypi/v/squirrel-battle
:target: https://pypi.org/project/squirrel-battle/ :target: https://pypi.org/project/squirrel-battle/
:alt: PyPI :alt: PyPI
.. image:: https://img.shields.io/pypi/dm/dungeon-battle .. image:: https://img.shields.io/pypi/dm/squirrel-battle
:target: https://pypi.org/project/squirrel-battle/ :target: https://pypi.org/project/squirrel-battle/
:alt: PyPI downloads :alt: PyPI downloads
@ -37,6 +37,9 @@ Bienvenue dans la documentation de Squirrel Battle !
install-dev install-dev
tests tests
display/index display/index
translation
deployment
documentation
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3

View File

@ -1,16 +1,19 @@
Installation d'un environnement de développement Installation d'un environnement de développement
================================================ ================================================
Il est toujours préférable de travailler dans un environnement Python isolé du reste de son instalation. Il est toujours préférable de travailler dans un environnement Python isolé du
reste de son instalation.
1. **Installation des dépendances de la distribution.** 1. **Installation des dépendances de la distribution.**
Vous devez déjà installer Python et le module qui permet de créer des environnements virtuels. Vous devez déjà installer Python et le module qui permet de créer des
On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre. environnements virtuels.
On donne ci-dessous l'exemple pour une distribution basée sur Debian,
mais vous pouvez facilement adapter pour ArchLinux ou autre.
.. code:: bash .. code:: bash
$ sudo apt update $ sudo apt update
$ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev git $ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev gettext git
2. **Clonage du dépot** là où vous voulez : 2. **Clonage du dépot** là où vous voulez :
@ -25,7 +28,13 @@ Il est toujours préférable de travailler dans un environnement Python isolé d
$ python3 -m venv env $ python3 -m venv env
$ source env/bin/activate # entrer dans l'environnement $ source env/bin/activate # entrer dans l'environnement
(env)$ pip3 install -r requirements.txt (env) $ pip3 install -r requirements.txt
(env)$ deactivate # sortir de l'environnement (env) $ deactivate # sortir de l'environnement
4. **Compilation des messages de traduction.**
.. code:: bash
(env) $ python3 main.py --compilemessages
Le lancement du jeu se fait en lançant la commande ``python3 main.py``. Le lancement du jeu se fait en lançant la commande ``python3 main.py``.

View File

@ -61,29 +61,19 @@ Le jeu peut être ensuite lancé via la commande ``squirrel-battle``.
Sur Ubuntu/Debian Sur Ubuntu/Debian
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
.. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14_all.deb?job=build-deb .. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14.1_all.deb?job=build-deb
Un paquet_ est généré par l'intégration continue de Gitlab à chaque commit. Un paquet_ est généré par l'intégration continue de Gitlab à chaque commit.
Ils sont également attachés à chaque nouvelle release. Ils sont également attachés à chaque nouvelle release.
Il dépend du paquet ``fonts-noto-color-emoji``, permettant d'afficher les émojis Il dépend du paquet ``fonts-noto-color-emoji``, permettant d'afficher les émojis
dans le terminal. Il peut être installé via APT normalement sur une distribution dans le terminal. Il peut être installé via APT.
récente, toutefois sur les versions les plus vieilles, incluant Debian Buster,
certains émojis n'apparaissent pas. Il est essentiel de maintenir ce paquet à
jour. Pour installer manuellement la dernière version de ce paquet,
il suffit d'exécuter :
.. code:: bash
wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb
dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb
rm fonts-noto-color-emoji_0~20200916-1_all.deb
Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` : Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` :
.. code:: bash .. code:: bash
dpkg -i python3-squirrelbattle_3.14_all.deb dpkg -i python3-squirrelbattle_3.14.1_all.deb
Ce paquet inclut un patch pour afficher les émojis écureuil correctement. Ce paquet inclut un patch pour afficher les émojis écureuil correctement.

View File

@ -12,7 +12,7 @@ Pack de textures
.. _Cœur: entities/items.html#coeur .. _Cœur: entities/items.html#coeur
.. _Bombe: entities/items.html#bombe .. _Bombe: entities/items.html#bombe
.. _Lapin: entities/monsters.html#lapin .. _Lapin: entities/monsters.html#lapin
.. _Castor: entities/monsters.html#castor .. _Tigre: entities/monsters.html#tigre
.. _Nounours: entities/monsters.html#nounours .. _Nounours: entities/monsters.html#nounours
Chaque entité_ et chaque tuile_ de la carte_ est associé à un caractère pour Chaque entité_ et chaque tuile_ de la carte_ est associé à un caractère pour
@ -42,7 +42,7 @@ Chaque tuile fait un caractère de large.
* Cœur_ : ```` * Cœur_ : ````
* Bombe_ : ``o`` * Bombe_ : ``o``
* Lapin_ : ``Y`` * Lapin_ : ``Y``
* Castor_ : ``_`` * Tigre_ : ``n``
* Nounours_ : ``8`` * Nounours_ : ``8``
@ -61,5 +61,5 @@ Chaque tuile fait 2 caractères de large pour afficher les émojis proprement.
* Cœur_ : ``💜`` * Cœur_ : ``💜``
* Bombe_ : ``💣`` * Bombe_ : ``💣``
* Lapin_ : ``🐇`` * Lapin_ : ``🐇``
* Castor_ : ``🦫`` * Tigre_ : ``🐅``
* Nounours_ : ``🧸`` * Nounours_ : ``🧸``

120
docs/translation.rst Normal file
View File

@ -0,0 +1,120 @@
Traduction
==========
Le jeu Squirrel Battle est entièrement traduit en anglais, en français et en allement.
La langue se choisit dans les `paramètres <settings.html>`_.
Utitisation
-----------
Les traductions sont gérées grâce au module natif ``gettext``. Le module
``squirrelbattle.translations`` s'occupe d'installer les traductions, et de
donner les chaînes traduites.
Pour choisir la langue, il faut appeler ``Translator.setlocale(language: str)``,
``language`` correspond au code à 2 lettres de la langue.
Enfin, le module expose une fonction ``gettext(str) -> str`` qui permet de
traduire les chaînes.
Il est courant et recommandé d'importer cette fonction sous l'alias ``_``,
afin de limiter la verbositer et de permettre de rendre facilement une chaîne
traduisible.
.. code:: python
from squirrelbattle.translations import gettext as _, Translator
Translator.setlocale("fr")
print(_("I am a translatable string"))
print("I am not translatable")
Si les traductions sont bien faites (voir ci-dessous), cela donnera :
.. code::
Je suis une chaîne traduisible
I am not translatable
À noter que si la chaîne n'est pas traduite, alors par défaut on renvoie la
chaîne elle-même.
Extraction des chaînes à traduire
---------------------------------
L'appel à ``gettext`` ne fait pas que traduire les chaînes : il est possible
également d'extraire toutes les chaînes à traduire.
Il est nécessaire d'installer le paquet Linux ``gettext`` pour cela.
L'utilitaire ``xgettext`` s'occupe de cette extraction. Il s'utilise de la façon
suivante :
.. code:: bash
xgettext --from-code utf-8 -o output_file.po source_1.py ... source_n.py
Afin de ne pas avoir à sélectionner manuellement chaque fichier, il est possible
d'appeler directement ``python3 main.py --makemessages``. Cela a pour effet
d'exécuter pour chaque langue ``<LANG>`` :
.. code:: bash
find squirrelbattle -iname '*.py' | xargs xgettext --from-code utf-8
--add-comments
--package-name=squirrelbattle
--package-version=3.14.1
"--copyright-holder=ÿnérant, eichhornchen, nicomarg, charlse"
--msgid-bugs-address=squirrel-battle@crans.org
-o squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po
Les fichiers de traductions se trouvent alors dans
``squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po``.
Traduire les chaînes
--------------------
Après extraction des chaînes, les chaînes à traduire se trouvent dans
``squirrelbattle/locale/<LANG>/LC_MESSAGES/squirrelbattle.po``, comme indiqué
ci-dessus.
Ce fichier peut-être édité avec un utilitaire tel que ``poedit``, sur
l'interface Web sur `<https://translate.ynerant.fr/squirrel-battle/squirrel-battle>`_,
mais surtout manuellement avec un éditeur de texte.
Dans ce fichier, on obtient pour chaque chaîne à traduire un paragraphe de la
forme :
.. code:: po
#: main.py:4
msgid "I am a translatable string"
msgstr "Je suis une chaîne traduisible"
Il sufift de remplir les champs ``msgstr``.
Compilation des chaînes
-----------------------
Pour gagner en efficacité, les chaînes sont compilées dans un fichier avec
l'extension ``.mo``. Ce sont ces fichiers qui sont lus par le module de traduction.
Pour compiler les traductions, c'est l'utilitaire ``msgfmt`` fourni toujours par
le paquet Linux ``gettext`` que nous utilisons. Il s'utilise assez simplement :
.. code:: bash
msgfmt po_file.po -o mo_file.mo
À nouveau, il est possible de compiler automatiquement les messages en exécutant
``python3 main.py --compilemessages``.
.. warning::
On ne partagera pas dans le dépôt Git les fichiers compilé. En développement,
on compilera soi-même les messages, et en production, la construction des
paquets se charge de compiler automatiquement les traductions.

View File

@ -24,21 +24,12 @@ Sous Ubuntu/Debian
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
À nouveau, le terminal `xfce4-terminal` est recommandé. Le paquet À nouveau, le terminal `xfce4-terminal` est recommandé. Le paquet
`fonts-noto-color-emoji`. Toutefois, le rythme de mise à jour de Debian étant `fonts-noto-color-emoji`.
lent, le paquet le plus récent ne contient pas tous les émojis. Sur Debian,
il faudra donc installer le paquet le plus récent, ce qui fonctionne sans
dépendance supplémentaire :
.. code:: bash Toutefois, un problème reste avec l'écureuil. Sous Ubuntu et Debian, le
caractère écureuil existe déjà, mais ne s'affiche pas proprement. On peut
wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb appliquer un patch qui permet d'afficher les émojis correctement dans son
dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb terminal. Pour cela, il suffit de faire :
rm fonts-noto-color-emoji_0~20200916-1_all.deb
Il reste le problème de l'écureuil. Sous Ubuntu et Debian, le caractère écureuil
existe déjà, mais ne s'affiche pas proprement. On peut appliquer un patch qui
permet d'afficher les émojis correctement dans son terminal. Pour cela, il
suffit de faire :
.. code:: bash .. code:: bash

24
main.py
View File

@ -1,5 +1,25 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import argparse
import sys
from squirrelbattle.bootstrap import Bootstrap from squirrelbattle.bootstrap import Bootstrap
from squirrelbattle.translations import Translator
if __name__ == "__main__": if __name__ == "__main__":
Bootstrap.run_game() parser = argparse.ArgumentParser()
parser.add_argument("--makemessages", "-mm", action="store_true",
help="Extract translatable strings")
parser.add_argument("--compilemessages", "-cm", action="store_true",
help="Compile translatable strings")
args = parser.parse_args(sys.argv[1:])
if args.makemessages:
Translator.makemessages()
elif args.compilemessages:
Translator.compilemessages()
else:
Bootstrap.run_game()

View File

@ -1,17 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import subprocess
from setuptools import find_packages, setup from setuptools import find_packages, setup
with open("README.md", "r") as f: with open("README.md", "r") as f:
long_description = f.read() long_description = f.read()
# Compile messages
for language in ["de", "en", "fr"]:
args = ["msgfmt", "--check-format",
"-o", f"squirrelbattle/locale/{language}/LC_MESSAGES"
"/squirrelbattle.mo",
f"squirrelbattle/locale/{language}/LC_MESSAGES"
"/squirrelbattle.po"]
print(f"Compiling {language} messages...")
subprocess.Popen(args)
setup( setup(
name="squirrel-battle", name="squirrel-battle",
version="3.14", version="3.14.1",
author="ynerant", author="ÿnérant, eichhornchen, nicomarg, charlse",
author_email="ynerant@crans.org", author_email="squirrel-battle@crans.org",
description="Watch out for squirrel's knifes!", description="Watch out for squirrel's knives!",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://gitlab.crans.org/ynerant/squirrel-battle", url="https://gitlab.crans.org/ynerant/squirrel-battle",
@ -32,7 +46,7 @@ setup(
], ],
python_requires='>=3.6', python_requires='>=3.6',
include_package_data=True, include_package_data=True,
package_data={"squirrelbattle": ["assets/*"]}, package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]},
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game", "squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game",

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.game import Game from squirrelbattle.game import Game
from squirrelbattle.display.display_manager import DisplayManager from squirrelbattle.display.display_manager import DisplayManager
from squirrelbattle.term_manager import TermManager from squirrelbattle.term_manager import TermManager

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from typing import Any, Optional, Union from typing import Any, Optional, Union
@ -19,6 +22,24 @@ class Display:
def newpad(self, height: int, width: int) -> Union[FakePad, Any]: def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
return curses.newpad(height, width) if self.screen else FakePad() return curses.newpad(height, width) if self.screen else FakePad()
def truncate(self, msg: str, height: int, width: int) -> str:
height = max(0, height)
width = max(0, width)
lines = msg.split("\n")
lines = lines[:height]
lines = [line[:width] for line in lines]
return "\n".join(lines)
def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None:
"""
Display a message onto the pad.
If the message is too large, it is truncated vertically and horizontally
"""
height, width = pad.getmaxyx()
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)
def init_pair(self, number: int, foreground: int, background: int) -> None: def init_pair(self, number: int, foreground: int, background: int) -> None:
return curses.init_pair(number, foreground, background) \ return curses.init_pair(number, foreground, background) \
if self.screen else None if self.screen else None
@ -32,14 +53,36 @@ class Display:
self.y = y self.y = y
self.width = width self.width = width
self.height = height self.height = height
if hasattr(self, "pad") and resize_pad: if hasattr(self, "pad") and resize_pad and \
self.pad.resize(self.height, self.width) self.height >= 0 and self.width >= 0:
self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None: def refresh(self, *args, resize_pad: bool = True) -> None:
if len(args) == 4: if len(args) == 4:
self.resize(*args, resize_pad) self.resize(*args, resize_pad)
self.display() self.display()
def refresh_pad(self, pad: Any, top_y: int, top_x: int,
window_y: int, window_x: int,
last_y: int, last_x: int) -> None:
"""
Refresh 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
is drawn and no error is raised.
"""
top_y, top_x = max(0, top_y), max(0, top_x)
window_y, window_x = max(0, window_y), max(0, window_x)
screen_max_y, screen_max_x = self.screen.getmaxyx() if self.screen \
else (42, 42)
last_y, last_x = min(screen_max_y - 1, last_y), \
min(screen_max_x - 1, last_x)
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)
def display(self) -> None: def display(self) -> None:
raise NotImplementedError raise NotImplementedError
@ -50,3 +93,68 @@ class Display:
@property @property
def cols(self) -> int: def cols(self) -> int:
return curses.COLS if self.screen else 42 return curses.COLS if self.screen else 42
class VerticalSplit(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, 1)
@property
def width(self) -> int:
return 1
@width.setter
def width(self, val: Any) -> None:
pass
def display(self) -> None:
for i in range(self.height):
self.addstr(self.pad, i, 0, "")
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x)
class HorizontalSplit(Display):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(1, self.cols)
@property
def height(self) -> int:
return 1
@height.setter
def height(self, val: Any) -> None:
pass
def display(self) -> None:
for i in range(self.width):
self.addstr(self.pad, 0, i, "")
self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y,
self.x + self.width - 1)
class Box(Display):
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)
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, self.height - 1, 0,
"" + "" * (self.width - 2) + "", self.pair)
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1)

View File

@ -1,5 +1,10 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit
from squirrelbattle.display.mapdisplay import MapDisplay from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.statsdisplay import StatsDisplay
from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \ from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \
MainMenuDisplay MainMenuDisplay
@ -22,9 +27,12 @@ class DisplayManager:
screen, pack) screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack)
self.logsdisplay = LogsDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack)
self.messagedisplay = MessageDisplay(screen=screen, pack=None)
self.hbar = HorizontalSplit(screen, pack)
self.vbar = VerticalSplit(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay, self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay, self.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay] self.logsdisplay, self.messagedisplay]
self.update_game_components() self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None: def handle_display_action(self, action: DisplayActions) -> None:
@ -40,20 +48,35 @@ class DisplayManager:
self.statsdisplay.update_player(self.game.player) self.statsdisplay.update_player(self.game.player)
self.settingsmenudisplay.update_menu(self.game.settings_menu) self.settingsmenudisplay.update_menu(self.game.settings_menu)
self.logsdisplay.update_logs(self.game.logs) self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message)
def refresh(self) -> None: def refresh(self) -> None:
if self.game.state == GameMode.PLAY: if self.game.state == GameMode.PLAY:
# The map pad has already the good size # The map pad has already the good size
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols, self.mapdisplay.refresh(0, 0, self.rows * 4 // 5,
self.mapdisplay.pack.tile_width
* (self.cols * 4 // 5
// self.mapdisplay.pack.tile_width),
resize_pad=False) resize_pad=False)
self.statsdisplay.refresh(self.rows * 4 // 5, 0, self.statsdisplay.refresh(0, self.cols * 4 // 5 + 1,
self.rows // 10, self.cols) self.rows, self.cols // 5 - 1)
self.logsdisplay.refresh(self.rows * 9 // 10, 0, self.logsdisplay.refresh(self.rows * 4 // 5 + 1, 0,
self.rows // 10, self.cols) self.rows // 5 - 1, self.cols * 4 // 5)
self.hbar.refresh(self.rows * 4 // 5, 0, 1, self.cols * 4 // 5)
self.vbar.refresh(0, self.cols * 4 // 5, self.rows, 1)
if self.game.state == GameMode.MAINMENU: if self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
if self.game.state == GameMode.SETTINGS: if self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1) self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1)
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
self.messagedisplay.refresh(y, x, height, width)
self.resize_window() self.resize_window()
def resize_window(self) -> bool: def resize_window(self) -> bool:

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.display.display import Display from squirrelbattle.display.display import Display
from squirrelbattle.interfaces import Logs from squirrelbattle.interfaces import Logs
@ -12,12 +15,11 @@ class LogsDisplay(Display):
self.logs = logs self.logs = logs
def display(self) -> None: def display(self) -> None:
print(type(self.logs.messages), flush=True)
messages = self.logs.messages[-self.height:] messages = self.logs.messages[-self.height:]
messages = messages[::-1] messages = messages[::-1]
self.pad.clear() self.pad.erase()
for i in range(min(self.height, len(messages))): for i in range(min(self.height, len(messages))):
self.pad.addstr(self.height - i - 1, self.x, self.addstr(self.pad, self.height - i - 1, self.x,
messages[i][:self.width]) messages[i][:self.width])
self.pad.refresh(0, 0, self.y, self.x, self.y + self.height, self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.x + self.width) self.y + self.height - 1, self.x + self.width - 1)

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.interfaces import Map from squirrelbattle.interfaces import Map
from .display import Display from .display import Display
@ -15,11 +17,11 @@ class MapDisplay(Display):
def update_pad(self) -> None: def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color) 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.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.pad.addstr(0, 0, self.map.draw_string(self.pack), self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1)) self.color_pair(1))
for e in self.map.entities: for e in self.map.entities:
self.pad.addstr(e.y, self.pack.tile_width * e.x, self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2)) self.pack[e.name.upper()], self.color_pair(2))
def display(self) -> None: def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
@ -31,9 +33,18 @@ class MapDisplay(Display):
smaxrow = min(smaxrow, self.height - 1) smaxrow = min(smaxrow, self.height - 1)
smaxcol = self.pack.tile_width * self.map.width - \ smaxcol = self.pack.tile_width * self.map.width - \
(x + deltax) + self.width - 1 (x + deltax) + self.width - 1
# Wrap perfectly the map according to the width of the tiles
pmincol = self.pack.tile_width * (pmincol // self.pack.tile_width)
smincol = self.pack.tile_width * (smincol // self.pack.tile_width)
smaxcol = self.pack.tile_width \
* (smaxcol // self.pack.tile_width + 1) - 1
smaxcol = min(smaxcol, self.width - 1) smaxcol = min(smaxcol, self.width - 1)
pminrow = max(0, min(self.map.height, pminrow)) pminrow = max(0, min(self.map.height, pminrow))
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol)) pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
self.pad.clear()
self.pad.erase()
self.update_pad() self.update_pad()
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow,
smaxcol)

View File

@ -1,8 +1,12 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import List from typing import List
from squirrelbattle.menus import Menu, MainMenu from squirrelbattle.menus import Menu, MainMenu
from .display import Display from .display import Display, Box
from ..resources import ResourceManager from ..resources import ResourceManager
from ..translations import gettext as _
class MenuDisplay(Display): class MenuDisplay(Display):
@ -11,25 +15,23 @@ class MenuDisplay(Display):
""" """
position: int position: int
def __init__(self, *args): def __init__(self, *args, **kwargs):
super().__init__(*args) super().__init__(*args, **kwargs)
self.menubox = self.newpad(self.rows, self.cols) self.menubox = Box(*args, **kwargs)
def update_menu(self, menu: Menu) -> None: def update_menu(self, menu: Menu) -> None:
self.menu = menu self.menu = menu
self.trueheight = len(self.values)
self.truewidth = max([len(a) for a in self.values])
# Menu values are printed in pad # Menu values are printed in pad
self.pad = self.newpad(self.trueheight, self.truewidth + 2) self.pad = self.newpad(self.trueheight, self.truewidth + 2)
for i in range(self.trueheight): for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i]) self.addstr(self.pad, i, 0, " " + self.values[i])
def update_pad(self) -> None: def update_pad(self) -> None:
for i in range(self.trueheight): for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i]) self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line # set a marker on the selected line
self.pad.addstr(self.menu.position, 0, ">") self.addstr(self.pad, self.menu.position, 0, ">")
def display(self) -> None: def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \ cornery = 0 if self.height - 2 >= self.menu.position - 1 \
@ -37,20 +39,21 @@ class MenuDisplay(Display):
if self.height - 2 >= self.trueheight - self.menu.position else 0 if self.height - 2 >= self.trueheight - self.menu.position else 0
# Menu box # Menu box
self.menubox.addstr(0, 0, "" + "" * (self.width - 2) + "") self.menubox.refresh(self.y, self.x, self.height, self.width)
for i in range(1, self.height - 1): self.pad.erase()
self.menubox.addstr(i, 0, "" + " " * (self.width - 2) + "")
self.menubox.addstr(self.height - 1, 0,
"" + "" * (self.width - 2) + "")
self.menubox.refresh(0, 0, self.y, self.x,
self.height + self.y,
self.width + self.x)
self.update_pad() self.update_pad()
self.pad.refresh(cornery, 0, self.y + 1, self.x + 2, self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2,
self.height - 2 + self.y, self.height - 2 + self.y,
self.width - 2 + self.x) self.width - 2 + self.x)
@property
def truewidth(self) -> int:
return max([len(str(a)) for a in self.values])
@property
def trueheight(self) -> int:
return len(self.values)
@property @property
def preferred_width(self) -> int: def preferred_width(self) -> int:
return self.truewidth + 6 return self.truewidth + 6
@ -70,9 +73,10 @@ class SettingsMenuDisplay(MenuDisplay):
""" """
@property @property
def values(self) -> List[str]: def values(self) -> List[str]:
return [a[1][1] + (" : " return [_(a[1][1]) + (" : "
+ ("?" if self.menu.waiting_for_key + ("?" if self.menu.waiting_for_key
and a == self.menu.validate() else a[1][0]) and a == self.menu.validate() else a[1][0]
.replace("\n", "\\n"))
if a[1][0] else "") for a in self.menu.values] if a[1][0] else "") for a in self.menu.values]
@ -95,9 +99,11 @@ class MainMenuDisplay(Display):
def display(self) -> None: def display(self) -> None:
for i in range(len(self.title)): for i in range(len(self.title)):
self.pad.addstr(4 + i, max(self.width // 2 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.pad.refresh(0, 0, self.y, self.x, self.height, self.width) self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)
menuwidth = min(self.menudisplay.preferred_width, self.width) menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1 menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
self.menudisplay.refresh( self.menudisplay.refresh(

View File

@ -0,0 +1,31 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.display import Box, Display
class MessageDisplay(Display):
"""
Display a message in a popup.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.box = Box(fg_border_color=curses.COLOR_RED, *args, **kwargs)
self.message = ""
self.pad = self.newpad(1, 1)
def update_message(self, msg: str) -> None:
self.message = msg
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.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.height + self.y - 1,
self.width + self.x - 1)

View File

@ -1,9 +1,12 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from ..entities.player import Player
from ..translations import gettext as _
from .display import Display from .display import Display
from squirrelbattle.entities.player import Player
class StatsDisplay(Display): class StatsDisplay(Display):
player: Player player: Player
@ -17,36 +20,28 @@ class StatsDisplay(Display):
self.player = p self.player = p
def update_pad(self) -> None: def update_pad(self) -> None:
string = "" string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
for _ in range(self.width - 1):
string = string + "-"
self.pad.addstr(0, 0, string)
string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\
.format(self.player.level, self.player.current_xp, .format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health, self.player.max_xp, self.player.health,
self.player.maxhealth) self.player.maxhealth)
for _ in range(self.width - len(string2) - 1): self.addstr(self.pad, 0, 0, string2)
string2 = string2 + " " string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
self.pad.addstr(1, 0, string2)
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
.format(self.player.strength, .format(self.player.strength,
self.player.intelligence, self.player.charisma, self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution) self.player.dexterity, self.player.constitution)
for _ in range(self.width - len(string3) - 1): self.addstr(self.pad, 3, 0, string3)
string3 = string3 + " "
self.pad.addstr(2, 0, string3)
inventory_str = "Inventaire : " + "".join( inventory_str = _("Inventory:") + " " + "".join(
self.pack[item.name.upper()] for item in self.player.inventory) self.pack[item.name.upper()] for item in self.player.inventory)
self.pad.addstr(3, 0, inventory_str) self.addstr(self.pad, 8, 0, inventory_str)
if self.player.dead: if self.player.dead:
self.pad.addstr(4, 0, "VOUS ÊTES MORT", self.addstr(self.pad, 10, 0, _("YOU ARE DEAD"),
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3)) | self.color_pair(3))
def display(self) -> None: def display(self) -> None:
self.pad.clear() self.pad.erase()
self.update_pad() self.update_pad()
self.pad.refresh(0, 0, self.y, self.x, self.refresh_pad(self.pad, 0, 0, self.y, self.x,
4 + self.y, self.width + self.x) self.y + self.height - 1, self.width + self.x - 1)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from typing import Any from typing import Any
@ -51,7 +54,7 @@ TexturePack.ASCII_PACK = TexturePack(
HEART='', HEART='',
BOMB='o', BOMB='o',
RABBIT='Y', RABBIT='Y',
BEAVER='_', TIGER='n',
TEDDY_BEAR='8', TEDDY_BEAR='8',
MERCHANT='M', MERCHANT='M',
SUNFLOWER='I', SUNFLOWER='I',
@ -67,12 +70,12 @@ TexturePack.SQUIRREL_PACK = TexturePack(
EMPTY=' ', EMPTY=' ',
WALL='🧱', WALL='🧱',
FLOOR='██', FLOOR='██',
PLAYER='🐿️', PLAYER='🐿️ ',
HEDGEHOG='🦔', HEDGEHOG='🦔',
HEART='💜', HEART='💜',
BOMB='💣', BOMB='💣',
RABBIT='🐇', RABBIT='🐇',
BEAVER='🦫', TIGER='🐅',
TEDDY_BEAR='🧸', TEDDY_BEAR='🧸',
MERCHANT='🦜', MERCHANT='🦜',
SUNFLOWER='🌻', SUNFLOWER='🌻',

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional from typing import Optional
from .player import Player from .player import Player

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice from random import choice
from .player import Player from .player import Player
@ -52,13 +55,13 @@ class Monster(FightingEntity):
break break
class Beaver(Monster): class Tiger(Monster):
""" """
A beaver monster A tiger monster
""" """
def __init__(self, strength: int = 2, maxhealth: int = 20, def __init__(self, strength: int = 2, maxhealth: int = 20,
*args, **kwargs) -> None: *args, **kwargs) -> None:
super().__init__(name="beaver", strength=strength, super().__init__(name="tiger", strength=strength,
maxhealth=maxhealth, *args, **kwargs) maxhealth=maxhealth, *args, **kwargs)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import randint from random import randint
from typing import Dict, Tuple from typing import Dict, Tuple

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto from enum import Enum, auto
from typing import Optional from typing import Optional

View File

@ -1,3 +1,7 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from json import JSONDecodeError
from random import randint from random import randint
from typing import Any, Optional from typing import Any, Optional
import json import json
@ -10,6 +14,7 @@ from .interfaces import Map, Logs
from .resources import ResourceManager from .resources import ResourceManager
from .settings import Settings from .settings import Settings
from . import menus from . import menus
from .translations import gettext as _, Translator
from typing import Callable from typing import Callable
@ -27,13 +32,15 @@ class Game:
Init the game. Init the game.
""" """
self.state = GameMode.MAINMENU self.state = GameMode.MAINMENU
self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings = Settings() self.settings = Settings()
self.settings.load_settings() self.settings.load_settings()
self.settings.write_settings() self.settings.write_settings()
Translator.setlocale(self.settings.LOCALE)
self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings_menu.update_values(self.settings) self.settings_menu.update_values(self.settings)
self.logs = Logs() self.logs = Logs()
self.message = None
def new_game(self) -> None: def new_game(self) -> None:
""" """
@ -55,7 +62,7 @@ class Game:
when the given key gets pressed. when the given key gets pressed.
""" """
while True: # pragma no cover while True: # pragma no cover
screen.clear() screen.erase()
screen.refresh() screen.refresh()
self.display_actions(DisplayActions.REFRESH) self.display_actions(DisplayActions.REFRESH)
key = screen.getkey() key = screen.getkey()
@ -68,6 +75,11 @@ class Game:
Indicates what should be done when the given key is pressed, Indicates what should be done when the given key is pressed,
according to the current game state. according to the current game state.
""" """
if self.message:
self.message = None
self.display_actions(DisplayActions.REFRESH)
return
if self.state == GameMode.PLAY: if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key) self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU: elif self.state == GameMode.MAINMENU:
@ -130,9 +142,24 @@ class Game:
""" """
Loads the game from a dictionary Loads the game from a dictionary
""" """
self.map.load_state(d) try:
# noinspection PyTypeChecker self.map.load_state(d)
self.player = self.map.find_entities(Player)[0] except KeyError:
self.message = _("Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted.")
os.unlink(ResourceManager.get_config_path("save.json"))
self.display_actions(DisplayActions.UPDATE)
return
players = self.map.find_entities(Player)
if not players:
self.message = _("No player was found on this map!\n"
"Maybe you died?")
self.player.health = 0
self.display_actions(DisplayActions.UPDATE)
return
self.player = players[0]
self.display_actions(DisplayActions.UPDATE) self.display_actions(DisplayActions.UPDATE)
def load_game(self) -> None: def load_game(self) -> None:
@ -142,7 +169,15 @@ class Game:
file_path = ResourceManager.get_config_path("save.json") file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path): if os.path.isfile(file_path):
with open(file_path, "r") as f: with open(file_path, "r") as f:
self.load_state(json.loads(f.read())) try:
state = json.loads(f.read())
self.load_state(state)
except JSONDecodeError:
self.message = _("The JSON file is not correct.\n"
"Your save seems corrupted. "
"It got deleted.")
os.unlink(file_path)
self.display_actions(DisplayActions.UPDATE)
def save_game(self) -> None: def save_game(self) -> None:
""" """

View File

@ -1,11 +1,13 @@
#!/usr/bin/env python # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
from random import randint # SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto from enum import Enum, auto
from math import sqrt from math import sqrt
from random import choice, randint from random import choice, randint
from typing import List, Optional, Any from typing import List, Optional, Any
from squirrelbattle.display.texturepack import TexturePack from .display.texturepack import TexturePack
from .translations import gettext as _
class Logs: class Logs:
@ -127,7 +129,7 @@ class Map:
""" """
Put randomly {count} hedgehogs on the map, where it is available. Put randomly {count} hedgehogs on the map, where it is available.
""" """
for _ in range(count): for ignored in range(count):
y, x = 0, 0 y, x = 0, 0
while True: while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1) y, x = randint(0, self.height - 1), randint(0, self.width - 1)
@ -319,16 +321,20 @@ class Entity:
""" """
return isinstance(self, FriendlyEntity) return isinstance(self, FriendlyEntity)
@property
def translated_name(self) -> str:
return _(self.name.replace("_", " "))
@staticmethod @staticmethod
def get_all_entity_classes(): def get_all_entity_classes():
""" """
Returns all entities subclasses Returns all entities subclasses
""" """
from squirrelbattle.entities.items import Heart, Bomb from squirrelbattle.entities.items import Heart, Bomb
from squirrelbattle.entities.monsters import Beaver, Hedgehog, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear Rabbit, TeddyBear
from squirrelbattle.entities.friendly import Merchant,Sunflower from squirrelbattle.entities.friendly import Merchant,Sunflower
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,Sunflower] return [Tiger, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,Sunflower]
@staticmethod @staticmethod
def get_all_entity_classes_in_a_dict() -> dict: def get_all_entity_classes_in_a_dict() -> dict:
@ -336,12 +342,12 @@ class Entity:
Returns all entities subclasses in a dictionary Returns all entities subclasses in a dictionary
""" """
from squirrelbattle.entities.player import Player from squirrelbattle.entities.player import Player
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \ from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear TeddyBear
from squirrelbattle.entities.items import Bomb, Heart from squirrelbattle.entities.items import Bomb, Heart
from squirrelbattle.entities.friendly import Merchant,Sunflower from squirrelbattle.entities.friendly import Merchant,Sunflower
return { return {
"Beaver": Beaver, "Tiger": Tiger,
"Bomb": Bomb, "Bomb": Bomb,
"Heart": Heart, "Heart": Heart,
"Hedgehog": Hedgehog, "Hedgehog": Hedgehog,
@ -399,8 +405,10 @@ class FightingEntity(Entity):
""" """
Deals damage to the opponent, based on the stats Deals damage to the opponent, based on the stats
""" """
return f"{self.name} hits {opponent.name}. "\ return _("{name} hits {opponent}.")\
+ opponent.take_damage(self, self.strength) .format(name=_(self.translated_name.capitalize()),
opponent=_(opponent.translated_name)) + " " + \
opponent.take_damage(self, self.strength)
def take_damage(self, attacker: "Entity", amount: int) -> str: def take_damage(self, attacker: "Entity", amount: int) -> str:
""" """
@ -409,8 +417,11 @@ class FightingEntity(Entity):
self.health -= amount self.health -= amount
if self.health <= 0: if self.health <= 0:
self.die() self.die()
return f"{self.name} takes {amount} damage."\ return _("{name} takes {amount} damage.")\
+ (f" {self.name} dies." if self.health <= 0 else "") .format(name=self.translated_name.capitalize(), amount=str(amount))\
+ (" " + _("{name} dies.")
.format(name=self.translated_name.capitalize())
if self.health <= 0 else "")
def die(self) -> None: def die(self) -> None:
""" """
@ -451,5 +462,5 @@ class FriendlyEntity(Entity):
def talk_to(self, player : Any) -> str: def talk_to(self, player : Any) -> str:
a = randint(0,len(self.dialogue_option)-1) a = randint(0,len(self.dialogue_option)-1)
return "The sunflower said : "+self.dialogue_option[a] return "The sunflower said : "+self.dialogue_option[a]

View File

@ -0,0 +1,166 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# 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-11-28 16:03+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: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Neu Spiel"
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Weitergehen"
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Laden"
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Speichern"
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Einstellungen"
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Verlassen"
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Haupttaste zum Obengehen"
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Sekundärtaste zum Obengehen"
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Haupttaste zum Untergehen"
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Sekundärtaste zum Untergehen"
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Haupttaste zum Linksgehen"
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Sekundärtaste zum Linksgehen"
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Haupttaste zum Rechtsgehen"
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Sekundärtaste zum Rechtsgehen"
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Menütaste"
#: squirrelbattle/tests/translations_test.py:45
msgid "Texture pack"
msgstr "Textur-Packung"
#: squirrelbattle/tests/translations_test.py:46
msgid "Language"
msgstr "Sprache"
#: squirrelbattle/tests/translations_test.py:49
msgid "player"
msgstr "Spieler"
#: squirrelbattle/tests/translations_test.py:51
msgid "tiger"
msgstr "Tiger"
#: squirrelbattle/tests/translations_test.py:52
msgid "hedgehog"
msgstr "Igel"
#: squirrelbattle/tests/translations_test.py:53
msgid "rabbit"
msgstr "Kanninchen"
#: squirrelbattle/tests/translations_test.py:54
msgid "teddy bear"
msgstr "Teddybär"
#: squirrelbattle/tests/translations_test.py:56
msgid "bomb"
msgstr "Bombe"
#: squirrelbattle/tests/translations_test.py:57
msgid "heart"
msgstr "Herz"
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Bestand:"
#: squirrelbattle/display/statsdisplay.py:39
msgid "YOU ARE DEAD"
msgstr "SIE WURDEN GESTORBEN"
#: squirrelbattle/interfaces.py:398
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} schlägt {opponent}."
#: squirrelbattle/interfaces.py:410
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} dies."
msgstr "{name} stirbt."
#: squirrelbattle/menus.py:71
msgid "Back"
msgstr "Zurück"
#: squirrelbattle/game.py:148
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
"In Ihrer Speicherdatei fehlen einige Schlüssel.\n"
"Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht."
#: squirrelbattle/game.py:156
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
"Auf dieser Karte wurde kein Spieler gefunden!\n"
"Vielleicht sind Sie gestorben?"
#: squirrelbattle/game.py:176
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
"Die JSON-Datei ist nicht korrekt.\n"
"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht."

View File

@ -0,0 +1,195 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# 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-11-28 16:03+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: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr ""
#: squirrelbattle/display/statsdisplay.py:39
msgid "YOU ARE DEAD"
msgstr ""
#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr ""
#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr ""
#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14
#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr ""
#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr ""
#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr ""
#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr ""
#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr ""
#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr ""
#: squirrelbattle/menus.py:71
msgid "Back"
msgstr ""
#: squirrelbattle/game.py:147 squirrelbattle/game.py:148
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
#: squirrelbattle/game.py:155 squirrelbattle/game.py:156
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
#: squirrelbattle/game.py:175 squirrelbattle/game.py:176
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21
#: squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr ""
#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23
#: squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr ""
#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr ""
#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr ""
#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr ""
#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr ""
#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr ""
#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr ""
#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:41
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr ""
#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:43
#: squirrelbattle/tests/translations_test.py:45
msgid "Texture pack"
msgstr ""
#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40
#: squirrelbattle/tests/translations_test.py:44
#: squirrelbattle/tests/translations_test.py:46
msgid "Language"
msgstr ""
#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} dies."
msgstr ""
#: squirrelbattle/tests/translations_test.py:47
#: squirrelbattle/tests/translations_test.py:49
msgid "player"
msgstr ""
#: squirrelbattle/tests/translations_test.py:49
#: squirrelbattle/tests/translations_test.py:51
msgid "tiger"
msgstr ""
#: squirrelbattle/tests/translations_test.py:50
#: squirrelbattle/tests/translations_test.py:52
msgid "hedgehog"
msgstr ""
#: squirrelbattle/tests/translations_test.py:51
#: squirrelbattle/tests/translations_test.py:53
msgid "rabbit"
msgstr ""
#: squirrelbattle/tests/translations_test.py:52
#: squirrelbattle/tests/translations_test.py:54
msgid "teddy bear"
msgstr ""
#: squirrelbattle/tests/translations_test.py:54
#: squirrelbattle/tests/translations_test.py:56
msgid "bomb"
msgstr ""
#: squirrelbattle/tests/translations_test.py:55
#: squirrelbattle/tests/translations_test.py:57
msgid "heart"
msgstr ""

View File

@ -0,0 +1,201 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# 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-11-28 16:03+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: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Inventaire :"
#: squirrelbattle/display/statsdisplay.py:39
msgid "YOU ARE DEAD"
msgstr "VOUS ÊTES MORT"
#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} frappe {opponent}."
#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14
#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nouvelle partie"
#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Continuer"
#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Sauvegarder"
#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Charger"
#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Paramètres"
#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Quitter"
#: squirrelbattle/menus.py:71
msgid "Back"
msgstr "Retour"
#: squirrelbattle/game.py:147 squirrelbattle/game.py:148
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
"Certaines clés de votre ficher de sauvegarde sont manquantes.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/game.py:155 squirrelbattle/game.py:156
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
"Aucun joueur n'a été trouvé sur la carte !\n"
"Peut-être êtes-vous mort ?"
#: squirrelbattle/game.py:175 squirrelbattle/game.py:176
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
"Le fichier JSON de sauvegarde est incorrect.\n"
"Votre sauvegarde semble corrompue. Elle a été supprimée."
#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21
#: squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Touche principale pour aller vers le haut"
#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23
#: squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Touche secondaire pour aller vers le haut"
#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25
#: squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Touche principale pour aller vers le bas"
#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27
#: squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Touche secondaire pour aller vers le bas"
#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29
#: squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Touche principale pour aller vers la gauche"
#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31
#: squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Touche secondaire pour aller vers la gauche"
#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33
#: squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Touche principale pour aller vers la droite"
#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35
#: squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Touche secondaire pour aller vers la droite"
#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37
#: squirrelbattle/tests/translations_test.py:41
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Touche pour valider un menu"
#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39
#: squirrelbattle/tests/translations_test.py:43
#: squirrelbattle/tests/translations_test.py:45
msgid "Texture pack"
msgstr "Pack de textures"
#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40
#: squirrelbattle/tests/translations_test.py:44
#: squirrelbattle/tests/translations_test.py:46
msgid "Language"
msgstr "Langue"
#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} dies."
msgstr "{name} meurt."
#: squirrelbattle/tests/translations_test.py:47
#: squirrelbattle/tests/translations_test.py:49
msgid "player"
msgstr "joueur"
#: squirrelbattle/tests/translations_test.py:49
#: squirrelbattle/tests/translations_test.py:51
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:50
#: squirrelbattle/tests/translations_test.py:52
msgid "hedgehog"
msgstr "hérisson"
#: squirrelbattle/tests/translations_test.py:51
#: squirrelbattle/tests/translations_test.py:53
msgid "rabbit"
msgstr "lapin"
#: squirrelbattle/tests/translations_test.py:52
#: squirrelbattle/tests/translations_test.py:54
msgid "teddy bear"
msgstr "nounours"
#: squirrelbattle/tests/translations_test.py:54
#: squirrelbattle/tests/translations_test.py:56
msgid "bomb"
msgstr "bombe"
#: squirrelbattle/tests/translations_test.py:55
#: squirrelbattle/tests/translations_test.py:57
msgid "heart"
msgstr "cœur"

View File

@ -1,9 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional
from .display.texturepack import TexturePack from .display.texturepack import TexturePack
from .enums import GameMode, KeyValues, DisplayActions from .enums import GameMode, KeyValues, DisplayActions
from .settings import Settings from .settings import Settings
from .translations import gettext as _, Translator
class Menu: class Menu:
@ -38,15 +42,15 @@ class MainMenuValues(Enum):
""" """
Values of the main menu Values of the main menu
""" """
START = 'Nouvelle partie' START = "New game"
RESUME = 'Continuer' RESUME = "Resume"
SAVE = 'Sauvegarder' SAVE = "Save"
LOAD = 'Charger' LOAD = "Load"
SETTINGS = 'Paramètres' SETTINGS = "Settings"
EXIT = 'Quitter' EXIT = "Exit"
def __str__(self): def __str__(self):
return self.value return _(self.value)
class MainMenu(Menu): class MainMenu(Menu):
@ -64,7 +68,7 @@ class SettingsMenu(Menu):
def update_values(self, settings: Settings) -> None: def update_values(self, settings: Settings) -> None:
self.values = list(settings.__dict__.items()) self.values = list(settings.__dict__.items())
self.values.append(("RETURN", ["", "Retour"])) self.values.append(("RETURN", ["", _("Back")]))
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None: game: Any) -> None:
@ -92,6 +96,12 @@ class SettingsMenu(Menu):
game.settings.TEXTURE_PACK) game.settings.TEXTURE_PACK)
game.settings.write_settings() game.settings.write_settings()
self.update_values(game.settings) self.update_values(game.settings)
elif option == "LOCALE":
game.settings.LOCALE = 'fr' if game.settings.LOCALE == 'en'\
else 'de' if game.settings.LOCALE == 'fr' else 'en'
Translator.setlocale(game.settings.LOCALE)
game.settings.write_settings()
self.update_values(game.settings)
else: else:
self.waiting_for_key = True self.waiting_for_key = True
self.update_values(game.settings) self.update_values(game.settings)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path from pathlib import Path

View File

@ -1,8 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import json import json
import locale
import os import os
from typing import Any, Generator from typing import Any, Generator
from .resources import ResourceManager from .resources import ResourceManager
from .translations import gettext as _
class Settings: class Settings:
@ -13,25 +18,17 @@ class Settings:
We can define the setting by simply use settings.TEXTURE_PACK = 'new_key' We can define the setting by simply use settings.TEXTURE_PACK = 'new_key'
""" """
def __init__(self): def __init__(self):
self.KEY_UP_PRIMARY = \ self.KEY_UP_PRIMARY = ['z', 'Main key to move up']
['z', 'Touche principale pour aller vers le haut'] self.KEY_UP_SECONDARY = ['KEY_UP', 'Secondary key to move up']
self.KEY_UP_SECONDARY = \ self.KEY_DOWN_PRIMARY = ['s', 'Main key to move down']
['KEY_UP', 'Touche secondaire pour aller vers le haut'] self.KEY_DOWN_SECONDARY = ['KEY_DOWN', 'Secondary key to move down']
self.KEY_DOWN_PRIMARY = \ self.KEY_LEFT_PRIMARY = ['q', 'Main key to move left']
['s', 'Touche principale pour aller vers le bas'] self.KEY_LEFT_SECONDARY = ['KEY_LEFT', 'Secondary key to move left']
self.KEY_DOWN_SECONDARY = \ self.KEY_RIGHT_PRIMARY = ['d', 'Main key to move right']
['KEY_DOWN', 'Touche secondaire pour aller vers le bas'] self.KEY_RIGHT_SECONDARY = ['KEY_RIGHT', 'Secondary key to move right']
self.KEY_LEFT_PRIMARY = \ self.KEY_ENTER = ['\n', 'Key to validate a menu']
['q', 'Touche principale pour aller vers la gauche'] self.TEXTURE_PACK = ['ascii', 'Texture pack']
self.KEY_LEFT_SECONDARY = \ self.LOCALE = [locale.getlocale()[0][:2], 'Language']
['KEY_LEFT', 'Touche secondaire pour aller vers la gauche']
self.KEY_RIGHT_PRIMARY = \
['d', 'Touche principale pour aller vers la droite']
self.KEY_RIGHT_SECONDARY = \
['KEY_RIGHT', 'Touche secondaire pour aller vers la droite']
self.KEY_ENTER = \
['\n', 'Touche pour valider un menu']
self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé']
def __getattribute__(self, item: str) -> Any: def __getattribute__(self, item: str) -> Any:
superattribute = super().__getattribute__(item) superattribute = super().__getattribute__(item)
@ -50,10 +47,10 @@ class Settings:
Retrieve the comment of a setting. Retrieve the comment of a setting.
""" """
if item in self.settings_keys: if item in self.settings_keys:
return object.__getattribute__(self, item)[1] return _(object.__getattribute__(self, item)[1])
for key in self.settings_keys: for key in self.settings_keys:
if getattr(self, key) == item: if getattr(self, key) == item:
return object.__getattribute__(self, key)[1] return _(object.__getattribute__(self, key)[1])
@property @property
def settings_keys(self) -> Generator[str, Any, None]: def settings_keys(self) -> Generator[str, Any, None]:

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses import curses
from types import TracebackType from types import TracebackType

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,7 +1,10 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest import unittest
from squirrelbattle.entities.items import Bomb, Heart, Item from squirrelbattle.entities.items import Bomb, Heart, Item
from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
from squirrelbattle.entities.player import Player from squirrelbattle.entities.player import Player
from squirrelbattle.interfaces import Entity, Map from squirrelbattle.interfaces import Entity, Map
from squirrelbattle.resources import ResourceManager from squirrelbattle.resources import ResourceManager
@ -36,17 +39,17 @@ class TestEntities(unittest.TestCase):
""" """
Test some random stuff with fighting entities. Test some random stuff with fighting entities.
""" """
entity = Beaver() entity = Tiger()
self.map.add_entity(entity) self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 20) self.assertEqual(entity.maxhealth, 20)
self.assertEqual(entity.maxhealth, entity.health) self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 2) self.assertEqual(entity.strength, 2)
for _ in range(9): for _ in range(9):
self.assertEqual(entity.hit(entity), self.assertEqual(entity.hit(entity),
"beaver hits beaver. beaver takes 2 damage.") "Tiger hits tiger. Tiger takes 2 damage.")
self.assertFalse(entity.dead) self.assertFalse(entity.dead)
self.assertEqual(entity.hit(entity), "beaver hits beaver. " self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
+ "beaver takes 2 damage. beaver dies.") + "Tiger takes 2 damage. Tiger dies.")
self.assertTrue(entity.dead) self.assertTrue(entity.dead)
entity = Rabbit() entity = Rabbit()
@ -67,8 +70,8 @@ class TestEntities(unittest.TestCase):
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health) self.assertEqual(old_health - entity.strength, self.player.health)
self.assertEqual(self.map.logs.messages[-1], self.assertEqual(self.map.logs.messages[-1],
f"{entity.name} hits {self.player.name}. \ f"{entity.name.capitalize()} hits {self.player.name}. \
{self.player.name} takes {entity.strength} damage.") {self.player.name.capitalize()} takes {entity.strength} damage.")
# Fight the rabbit # Fight the rabbit
old_health = entity.health old_health = entity.health

View File

@ -1,13 +1,19 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import os import os
import unittest import unittest
from squirrelbattle.bootstrap import Bootstrap from ..bootstrap import Bootstrap
from squirrelbattle.display.display import Display from ..display.display import Display
from squirrelbattle.display.display_manager import DisplayManager from ..display.display_manager import DisplayManager
from squirrelbattle.entities.player import Player from ..entities.player import Player
from squirrelbattle.game import Game, KeyValues, GameMode from ..enums import DisplayActions
from squirrelbattle.menus import MainMenuValues from ..game import Game, KeyValues, GameMode
from squirrelbattle.settings import Settings from ..menus import MainMenuValues
from ..resources import ResourceManager
from ..settings import Settings
from ..translations import gettext as _, Translator
class TestGame(unittest.TestCase): class TestGame(unittest.TestCase):
@ -38,6 +44,27 @@ class TestGame(unittest.TestCase):
new_state = self.game.save_state() new_state = self.game.save_state()
self.assertEqual(old_state, new_state) self.assertEqual(old_state, new_state)
# Error on loading save
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write("I am not a JSON file")
self.assertIsNone(self.game.message)
self.game.load_game()
self.assertIsNotNone(self.game.message)
self.game.message = None
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write("{}")
self.assertIsNone(self.game.message)
self.game.load_game()
self.assertIsNotNone(self.game.message)
self.game.message = None
# Load game with a dead player
self.game.map.remove_entity(self.game.player)
self.game.save_game()
self.game.load_game()
self.assertIsNotNone(self.game.message)
def test_bootstrap_fail(self) -> None: def test_bootstrap_fail(self) -> None:
""" """
Ensure that the test can't play the game, Ensure that the test can't play the game,
@ -247,12 +274,23 @@ class TestGame(unittest.TestCase):
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii") self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
# Change language
Translator.compilemessages()
Translator.refresh_translations()
self.game.settings.LOCALE = "en"
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "fr")
self.assertEqual(_("New game"), "Nouvelle partie")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "de")
self.assertEqual(_("New game"), "Neu Spiel")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "en")
self.assertEqual(_("New game"), "New game")
# Navigate to "back" button # Navigate to "back" button
self.game.handle_key_pressed(KeyValues.DOWN) self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER) self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU) self.assertEqual(self.game.state, GameMode.MAINMENU)
@ -289,3 +327,13 @@ class TestGame(unittest.TestCase):
Check that some functions are not implemented, only for coverage. Check that some functions are not implemented, only for coverage.
""" """
self.assertRaises(NotImplementedError, Display.display, None) self.assertRaises(NotImplementedError, Display.display, None)
def test_messages(self) -> None:
"""
Display error messages.
"""
self.game.message = "I am an error"
self.game.display_actions(DisplayActions.UPDATE)
self.game.display_actions(DisplayActions.REFRESH)
self.game.handle_key_pressed(None, "random key")
self.assertIsNone(self.game.message)

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest import unittest
from squirrelbattle.display.texturepack import TexturePack from squirrelbattle.display.texturepack import TexturePack

View File

@ -1,3 +1,9 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Tuple
class FakePad: class FakePad:
""" """
In order to run tests, we simulate a fake curses pad that accepts functions In order to run tests, we simulate a fake curses pad that accepts functions
@ -10,8 +16,11 @@ class FakePad:
smincol: int, smaxrow: int, smaxcol: int) -> None: smincol: int, smaxrow: int, smaxcol: int) -> None:
pass pass
def clear(self) -> None: def erase(self) -> None:
pass pass
def resize(self, height: int, width: int) -> None: def resize(self, height: int, width: int) -> None:
pass pass
def getmaxyx(self) -> Tuple[int, int]:
return 42, 42

View File

@ -1,3 +1,6 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest import unittest
from squirrelbattle.settings import Settings from squirrelbattle.settings import Settings
@ -21,7 +24,7 @@ class TestSettings(unittest.TestCase):
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK), self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
settings.get_comment('TEXTURE_PACK')) settings.get_comment('TEXTURE_PACK'))
self.assertEqual(settings.get_comment(settings.TEXTURE_PACK), self.assertEqual(settings.get_comment(settings.TEXTURE_PACK),
'Pack de textures utilisé') 'Texture pack')
settings.TEXTURE_PACK = 'squirrel' settings.TEXTURE_PACK = 'squirrel'
self.assertEqual(settings.TEXTURE_PACK, 'squirrel') self.assertEqual(settings.TEXTURE_PACK, 'squirrel')

View File

@ -0,0 +1,57 @@
import unittest
from squirrelbattle.translations import gettext as _, Translator
class TestTranslations(unittest.TestCase):
def setUp(self) -> None:
Translator.compilemessages()
Translator.refresh_translations()
Translator.setlocale("fr")
def test_main_menu_translation(self) -> None:
"""
Ensure that the main menu is translated.
"""
self.assertEqual(_("New game"), "Nouvelle partie")
self.assertEqual(_("Resume"), "Continuer")
self.assertEqual(_("Load"), "Charger")
self.assertEqual(_("Save"), "Sauvegarder")
self.assertEqual(_("Settings"), "Paramètres")
self.assertEqual(_("Exit"), "Quitter")
def test_settings_menu_translation(self) -> None:
"""
Ensure that the settings menu is translated.
"""
self.assertEqual(_("Main key to move up"),
"Touche principale pour aller vers le haut")
self.assertEqual(_("Secondary key to move up"),
"Touche secondaire pour aller vers le haut")
self.assertEqual(_("Main key to move down"),
"Touche principale pour aller vers le bas")
self.assertEqual(_("Secondary key to move down"),
"Touche secondaire pour aller vers le bas")
self.assertEqual(_("Main key to move left"),
"Touche principale pour aller vers la gauche")
self.assertEqual(_("Secondary key to move left"),
"Touche secondaire pour aller vers la gauche")
self.assertEqual(_("Main key to move right"),
"Touche principale pour aller vers la droite")
self.assertEqual(_("Secondary key to move right"),
"Touche secondaire pour aller vers la droite")
self.assertEqual(_("Key to validate a menu"),
"Touche pour valider un menu")
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(_("rabbit"), "lapin")
self.assertEqual(_("teddy bear"), "nounours")
self.assertEqual(_("bomb"), "bombe")
self.assertEqual(_("heart"), "cœur")

View File

@ -0,0 +1,96 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import gettext as gt
import os
import subprocess
from pathlib import Path
from typing import Any, List
class Translator:
"""
This module uses gettext to translate strings.
Translator.setlocale defines the language of the strings,
then gettext() translates the message.
"""
SUPPORTED_LOCALES: List[str] = ["de", "en", "fr"]
locale: str = "en"
translators: dict = {}
@classmethod
def refresh_translations(cls) -> None:
"""
Load compiled translations.
"""
for language in cls.SUPPORTED_LOCALES:
rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES"
rep.mkdir(parents=True) if not rep.is_dir() else None
if os.path.isfile(rep / "squirrelbattle.mo"):
cls.translators[language] = gt.translation(
"squirrelbattle",
localedir=Path(__file__).parent / "locale",
languages=[language],
)
@classmethod
def setlocale(cls, lang: str) -> None:
"""
Define the language used to translate the game.
The language must be supported, otherwise nothing is done.
"""
lang = lang[:2]
if lang in cls.SUPPORTED_LOCALES:
cls.locale = lang
@classmethod
def get_translator(cls) -> Any:
return cls.translators.get(cls.locale, gt.NullTranslations())
@classmethod
def makemessages(cls) -> None: # pragma: no cover
"""
Analyse all strings in the project and extract them.
"""
for language in cls.SUPPORTED_LOCALES:
file_name = Path(__file__).parent / "locale" / language \
/ "LC_MESSAGES" / "squirrelbattle.po"
args = ["find", "squirrelbattle", "-iname", "*.py"]
find = subprocess.Popen(args, cwd=Path(__file__).parent.parent,
stdout=subprocess.PIPE)
args = ["xargs", "xgettext", "--from-code", "utf-8",
"--add-comments",
"--package-name=squirrelbattle",
"--package-version=3.14.1",
"--copyright-holder=ÿnérant, eichhornchen, "
"nicomarg, charlse",
"--msgid-bugs-address=squirrel-battle@crans.org",
"-o", file_name]
if file_name.is_file():
args.append("--join-existing")
print(f"Make {language} messages...")
subprocess.Popen(args, stdin=find.stdout).wait()
@classmethod
def compilemessages(cls) -> None:
"""
Compile translation messages from source files.
"""
for language in cls.SUPPORTED_LOCALES:
args = ["msgfmt", "--check-format",
"-o", Path(__file__).parent / "locale" / language
/ "LC_MESSAGES" / "squirrelbattle.mo",
Path(__file__).parent / "locale" / language
/ "LC_MESSAGES" / "squirrelbattle.po"]
print(f"Compiling {language} messages...")
subprocess.Popen(args).wait()
def gettext(message: str) -> str:
"""
Translate a message.
"""
return Translator.get_translator().gettext(message)
Translator.refresh_translations()