Merge branch 'master' into map_generation

# Conflicts:
#	dungeonbattle/game.py
#	squirrelbattle/mapgeneration/__init__.py
#	squirrelbattle/mapgeneration/randomwalk.py
This commit is contained in:
Yohann D'ANELLO 2020-12-11 02:14:47 +01:00
commit 7db2dd2b46
94 changed files with 5326 additions and 1243 deletions

15
.gitignore vendored
View File

@ -8,6 +8,21 @@ venv/
.pytest_cache/ .pytest_cache/
__pycache__ __pycache__
*.pyc
# Ignore build data
build/
dist/
*.egg-info/
# Don't commit settings # Don't commit settings
settings.json settings.json
# Don't commit game save
save.json
# Don't commit docs output
docs/_build
# Don't commit compiled messages
*.mo

View File

@ -1,11 +1,13 @@
stages: stages:
- test - test
- quality-assurance - quality-assurance
- build
py37: 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
@ -13,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
@ -21,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
@ -31,3 +35,18 @@ linters:
- pip install tox - pip install tox
script: tox -e linters script: tox -e linters
allow_failure: true allow_failure: true
build-deb:
image: debian:buster-slim
stage: build
before_script:
- apt-get update && apt-get -y --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools
script:
- dpkg-buildpackage
- mkdir build && cp ../*.deb build/
artifacts:
paths:
- build/*.deb
expire_in: 1 week
only:
- master

674
COPYING Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
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.
Squirrel Battle
Copyright (C) 2020 ÿnérant, eichhornchen, nicomarg, charlse
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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Squirrel Battle Copyright (C) 2020 ÿnérant, eichhornchen, nicomarg, charlse
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -1,35 +1,15 @@
[![pipeline status](https://gitlab.crans.org/ynerant/dungeon-battle/badges/master/pipeline.svg)](https://gitlab.crans.org/ynerant/dungeon-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/dungeon-battle/badges/master/coverage.svg)](https://gitlab.crans.org/ynerant/dungeon-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)
[![PyPI](https://img.shields.io/pypi/v/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
[![PYPI downloads](https://img.shields.io/pypi/dm/squirrel-battle)](https://pypi.org/project/squirrel-battle/)
[![AUR version](https://img.shields.io/aur/version/python-squirrel-battle)](https://aur.archlinux.org/packages/python-squirrel-battle/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
# Dungeon Battle # Squirrel Battle
M1 Software engineering project Attention aux couteaux des écureuils !
## Création d'un environnement de développement ## Documentation
Il est toujours préférable de travailler dans un environnement Python isolé du reste de son instalation. La documentation du projet est présente sur [squirrel-battle.readthedocs.io](https://squirrel-battle.readthedocs.io).
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.
On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre.
```bash
$ sudo apt update
$ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev git
```
2. **Clonage du dépot** là où vous voulez :
```bash
$ git clone git@gitlab.crans.org:ynerant/dungeon-battle.git && cd dungeon-battle
```
3. **Création d'un environment de travail Python décorrélé du système.**
On n'utilise pas `--system-site-packages` ici pour ne pas avoir des clashs de versions de modules avec le système.
```bash
$ python3 -m venv env
$ source env/bin/activate # entrer dans l'environnement
(env)$ pip3 install -r requirements.txt
(env)$ deactivate # sortir de l'environnement
```

118
debian/75-fix-squirrel-emojis.conf vendored Normal file
View File

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

5
debian/README.debian vendored Normal file
View File

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

11
debian/changelog vendored Normal file
View File

@ -0,0 +1,11 @@
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.
-- Yohann D'ANELLO <squirrel-battle@crans.org> Thu, 19 Nov 2020 03:30:42 +0100

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
10

16
debian/control vendored Normal file
View File

@ -0,0 +1,16 @@
Source: python3-squirrel-battle
Section: devel
Priority: optional
Maintainer: ynerant <squirrel-battle@crans.org>
Build-Depends: debhelper (>=10~), dh-python, gettext, python3-all, python3-setuptools
Depends: fonts-noto-color-emoji
Standards-Version: 4.1.4
Homepage: https://gitlab.crans.org/ynerant/squirrel-battle
X-Python3-Version: >= 3.6
Package: python3-squirrel-battle
Architecture: all
Multi-Arch: foreign
Depends: fonts-noto-color-emoji, ${python3:Depends}
Description: Squirrel Battle
Watch out for squirrel's knifes!

29
debian/copyright vendored Normal file
View File

@ -0,0 +1,29 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ÿnérant, eichhornchen, nicomarg, charlse
Upstream-Contact: ÿnérant, eichhornchen, nicomarg, charlse <squirrel-battle@crans.org>
Source: https://gitlab.crans.org/ynerant/squirrel-battle
Files: *
Copyright: 2020 ÿnérant, eichhornchen, nicomarg, charlse <squirrel-battle@crans.org>
License: GPL-3+
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 the Free Software Foundation; either
version 3 of the License, or (at your option) any later
version.
.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more
details.
.
You should have received a copy of the GNU General Public
License along with this package; if not, write to the Free
Software Foundation, Inc., 51 Franklin St, Fifth Floor,
Boston, MA 02110-1301 USA
.
On Debian systems, the full text of the GNU General Public
License version 3 can be found in the file
`/usr/share/common-licenses/GPL-3'.

2
debian/install vendored Normal file
View File

@ -0,0 +1,2 @@
debian/75-fix-squirrel-emojis.conf etc/fonts/conf.avail
debian/75-fix-squirrel-emojis.conf etc/fonts/conf.d

5
debian/rules vendored Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
%:
dh $@ --with python3 --buildsystem=pybuild

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

60
docs/conf.py Normal file
View File

@ -0,0 +1,60 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'Squirrel Battle'
copyright = "2020"
author = "Yohann D'ANELLO,\nMathilde DEPRES,\nNicolas MARGULIES,\nCharles PEYRAT"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx_rtd_theme",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'fr'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

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", "es", "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.

21
docs/display/index.rst Normal file
View File

@ -0,0 +1,21 @@
Gestion de l'affichage
======================
.. _curses: https://docs.python.org/3/howto/curses.html
L'intégralité de l'affichage du jeu est géré grâce à la bibliothèque native de
Python curses_.
.. warning::
Plus de documentation à venir.
.. toctree::
:maxdepth: 3
:caption: Affichage
menu
map
stats
logs

4
docs/display/logs.rst Normal file
View File

@ -0,0 +1,4 @@
Affichage de l'historique
=========================
Pas encore documenté.

4
docs/display/map.rst Normal file
View File

@ -0,0 +1,4 @@
Affichage de la carte
=====================
Pas encore documenté.

4
docs/display/menu.rst Normal file
View File

@ -0,0 +1,4 @@
Affichage des menus
===================
Pas encore documenté.

4
docs/display/stats.rst Normal file
View File

@ -0,0 +1,4 @@
Affichage des statistiques
==========================
Pas encore documenté.

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.

79
docs/entities/index.rst Normal file
View File

@ -0,0 +1,79 @@
Entités
=======
.. toctree::
:maxdepth: 3
:caption: Entités
player
monsters
items
Entité
------
Une entité est un élément placé sur la carte. Ce peut être le joueur, un monstre
ou bien un objet sur la carte. Chaque entité dispose des attributs suivants :
* ``name: str``
Il s'agit du type de l'entité.
* ``y: int``
* ``x: int``
Cela représente les coordonnées de l'entité sur la carte.
* ``map: Map``
Il s'agit de la carte sur laquelle est placée l'entité.
.. _objet: items.html
Il existe à l'heure actuelle deux types d'entité : une `entité attaquante`_ ou
bien un objet_.
Entité attaquante
-----------------
.. _monstre: monsters.html
.. _joueur: player.html
Une entité attaquante (``FightingEntity``) est un type d'entités représentant
les personnages présents sur la carte, pouvant alors se battre. Ce peut être
un monstre_ ou bien le joueur_.
Elles disposent toutes, en plus des paramètres d'entité, des attributs suivants :
* ``maxhealth: int``
Représente la vie maximale de l'entité, qui est aussi la vie de départ.
* ``health: int``
Représente la vie actuelle de l'entité.
* ``strength: int``
Représente la force de l'entité, le nombre de dégâts à faire à chaque coup.
* ``intelligence: int``
* ``charisma: int``
* ``dexterity: int``
* ``constitution: int``
Tous ces paramètres sont des statistiques de l'entité, n'ayant pas de réelle
influence pour le moment.
* ``level: int``
Niveau de l'entité.
Chaque type d'entité disposera de ses propres attributs de départ.
On considère une entité comme morte à partir du moment où sa vie descend
en-dessous de 0 point de vie. À ce moment-là, l'entité est retirée de la carte.
Lorsqu'une entité en frappe une autre, celle-ci inflige autant de dégâts qu'elle
n'a de force, et autant de points de vie sont perdus.

50
docs/entities/items.rst Normal file
View File

@ -0,0 +1,50 @@
Objets
======
.. _joueur: player.html
.. _pack de textures: ../texture_pack.html
Un objet est une entité présente sur la carte que le joueur_ peut ramasser.
Il lui suffit pour cela de s'approcher, et une fois sur la case de l'objet,
celui-ci est placé dans l'inventaire.
Un objet dispose de deux paramètres :
* ``held: bool``
Indique si l'objet est placé dans l'inventaire ou s'il est au sol.
* ``held_by: Optional[Player]``
Si l'objet est dans l'inventaire, renvoie son propriétaire.
Deux types d'objets sont pour l'instant présents :
Bombe
-----
.. _entités attaquantes: index.html#entite-attaquante
Une bombe est un objet que l'on peut ramasser. Une fois ramassée, elle est placée
dans l'inventaire. Le joueur peut ensuite lâcher la bombe, qui fera alors
3 dégâts à toutes les `entités attaquantes`_ situées à moins de une case.
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ``o``
et dans le `pack de textures`_ écureuil par l'émoji ``💣``.
.. note::
La gestion de l'inventaire n'ayant pas encore été implémentée, il n'est à
l'heure actuelle pas possible de lancer une bombe.
Cœur
----
Une cœur est un objet que l'on ne peut pas ramasser. Dès que le joueur s'en
approche, il est régénéré automatiquement de 3 points de vie, et le cœur disparaît.
Elle est représentée dans le `pack de textures`_ ASCII par le caractère ````
et dans le `pack de textures`_ écureuil par l'émoji ``💜``.

View File

@ -0,0 +1,55 @@
Monstres
========
.. _`entité attaquante`: index.html#entites-attaquantes
.. _`pack de textures`: ../texture-pack.html
Chaque monstre est une `entité attaquante`_, et hérite donc de ses attributs.
À chaque tick de jeu, chaque monstre se déplace d'une case, si possible.
Si le monstre est loin du joueur, ce déplacement est fait aléatoirement.
Sinon, si le monstre est à moins de 5 cases du joueur, alors il se dirige
au plus vite sur le joueur pour le frapper selon l'algorithme de Dijkstra,
et s'il est suffisamment proche frappe le joueur et lui fait autant de dégâts
qu'il n'a de force.
On dénombre actuellement 4 types de monstres :
Hérisson
--------
Son nom est fixé à `hedghog`. Il a par défaut une force à **3** et **10** points de vie.
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 ``🦔``.
Tigre
-----
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 ``n``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐅``.
Lapin
-----
Son nom est fixé à `rabbit`. Il a par défaut une force à **1** et **15** points de vie.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``Y``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐇``.
Nounours
--------
Son nom est fixé à `teddy_bear`. Il n'a pas de force et **50** points de vie.
Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``8``.
Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🧸``.

52
docs/entities/player.rst Normal file
View File

@ -0,0 +1,52 @@
Joueur
======
.. _`entité attaquante`: index.html#entites-attaquantes
.. _`paramètres`: ../settings.html
.. _`pack de textures`: ../texture-pack.html
.. _`objet`: items.html
Le joueur est une `entité attaquante`_, contrôlée par l'utilisateur humain.
Il est représenté dans le `pack de textures`_ ASCII par le caractère ``@``,
et dans le `pack de textures`_ écureuil par le fameux émoji écureuil ``🐿``.
En plus des attributs d'une `entité attaquante`_, le joueur dispose des atrributs
supplémentaires :
* ``current_xp: int``
Correspond à l'expérience accumulée par le joueur depuis le dernier niveau obtenu.
* ``max_xp: int``
Expérience requise au joueur pour changer de niveau. Vaut 10 fois le niveau actuel.
* ``inventory: List[Item]``
Contient l'ensemble des objets détenus par le joueur.
Déplacement
-----------
Selon les paramètres_, il est possible de bouger le joueur dans les 4 directions
en appuyant sur ``z``, ``q``, ``s``, ``d`` ou sur les flèches directionnelles.
Le joueur se retrouvera bloqué s'il avance contre un mur. Si il avance sur un
objet_, alors il prend l'objet_ et avance sur la case.
S'il rencontre une autre `entité attaquante`_, alors il frappe l'entité en
infligeant autant de dégâts qu'il n'a de force. À chaque fois qu'une entité est
tuée, le joueur gagne aléatoirement entre 3 et 7 points d'expérience.
Expérience
----------
À chaque monstre tué, le joueur gagne entre 3 et 7 points d'expérience aléatoirement.
Lorsque le joueur atteint la quantité d'expérience requise pour monter de niveau,
le joueur gagne un niveau, regagne toute sa vie, consomme son expérience et la
nouvelle quantité d'expérience requise est 10 fois le niveau actuel. De plus,
entre 5 et 10 fois le niveau actuel entités apparaissent aléatoirement sur la
carte à la montée de niveau. Enfin, le joueur gagne en force en montant de niveau.

54
docs/index.rst Normal file
View File

@ -0,0 +1,54 @@
Bienvenue dans la documentation de Squirrel Battle !
====================================================
.. image:: https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/pipeline.svg
:target: https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master
:alt: Pipeline status
.. image:: https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/coverage.svg
:target: https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master
:alt: Coverage report
.. image:: https://readthedocs.org/projects/squirrel-battle/badge/?version=latest
:target: https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/pypi/v/squirrel-battle
:target: https://pypi.org/project/squirrel-battle/
:alt: PyPI
.. image:: https://img.shields.io/pypi/dm/squirrel-battle
:target: https://pypi.org/project/squirrel-battle/
:alt: PyPI downloads
.. image:: https://img.shields.io/aur/version/python-squirrel-battle
:target: https://aur.archlinux.org/packages/python-squirrel-battle/
:alt: AUR version
.. image:: https://img.shields.io/badge/License-GPL%20v3-blue.svg
:target: https://www.gnu.org/licenses/gpl-3.0.txt
:alt: License: GPL v3
.. toctree::
:maxdepth: 3
:caption: Développer
install-dev
tests
display/index
translation
deployment
documentation
.. toctree::
:maxdepth: 3
:caption: Jouer
install
rules
map
entities/index
texture-pack
settings
troubleshooting

40
docs/install-dev.rst Normal file
View File

@ -0,0 +1,40 @@
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.
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.
On donne ci-dessous l'exemple pour une distribution basée sur Debian,
mais vous pouvez facilement adapter pour ArchLinux ou autre.
.. code:: bash
$ sudo apt update
$ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev gettext git
2. **Clonage du dépot** là où vous voulez :
.. code:: bash
$ git clone git@gitlab.crans.org:ynerant/squirrel-battle.git && cd squirrel-battle
3. **Création d'un environment de travail Python décorrélé du système.**
On n'utilise pas `--system-site-packages` ici pour ne pas avoir des clashs de versions de modules avec le système.
.. code:: bash
$ python3 -m venv env
$ source env/bin/activate # entrer dans l'environnement
(env) $ pip3 install -r requirements.txt
(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``.

80
docs/install.rst Normal file
View File

@ -0,0 +1,80 @@
Installation client
===================
Installation
------------
Différents paquets sont déployés, dans PyPI pour tout système utilisant Python,
un paquet Debian et un paquet Arch Linux.
Depuis PIP
~~~~~~~~~~
.. _PyPI: https://pypi.org/project/squirrel-battle/
Le projet `Squirrel Battle` est déployé dans PyPI_. Il suffit d'installer
Squirrel Battle en exécutant :
.. code:: bash
pip install --user squirrel-battle
Les mises à jour s'obtiennent également via PIP en exécutant :
.. code:: bash
pip install --user --upgrade squirrel-battle
Le jeu peut se lancer ensuite en exécutant la commande ``squirrel-battle``.
Toutefois, le paquet PyPI n'inclut pas les polices d'émojis. Il est recommandé
d'installer des polices telles que ``noto-fonts-emoji`` afin de prendre en charge
les émojis dans votre terminal.
Sur Arch Linux
~~~~~~~~~~~~~~
.. _AUR: https://aur.archlinux.org/
.. _python-squirrel-battle: https://aur.archlinux.org/packages/python-squirrel-battle/
.. _python-squirrel-battle-git: https://aur.archlinux.org/packages/python-squirrel-battle-git/
.. _yay: https://aur.archlinux.org/packages/yay/
Deux paquets sont publiés dans l'AUR_ (Arch User Repository) :
- python-squirrel-battle_
- python-squirrel-battle-git_
Le premier paquet est mis à jour à chaque nouvelle version déployée, le second
est utile pour des fins de développement et est en permanence à jour
avec la branche ``master`` du Git.
Les deux ne sont pas présents dans les dépôts officiels de Arch Linux, mais vous
pouvez les récupérer avec un outil tel que yay_.
Les paquets incluent la dépendance ``noto-fonts-emoji``, qui permet d'afficher
les émojis dans le terminal.
Le jeu peut être ensuite lancé via la commande ``squirrel-battle``.
Sur Ubuntu/Debian
~~~~~~~~~~~~~~~~~
.. _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.
Ils sont également attachés à chaque nouvelle release.
Il dépend du paquet ``fonts-noto-color-emoji``, permettant d'afficher les émojis
dans le terminal. Il peut être installé via APT.
Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` :
.. code:: bash
dpkg -i python3-squirrelbattle_3.14.1_all.deb
Ce paquet inclut un patch pour afficher les émojis écureuil correctement.
Après cela, le jeu peut être lancé grâce à la commande ``squirrel-battle``.

46
docs/map.rst Normal file
View File

@ -0,0 +1,46 @@
Carte
=====
.. _entités: entity/index.html
.. _pack de textures: texture-pack.html
Dans Squirrel game, le joueur se déplace dans un donjon, constitué de plusieurs
cartes. Pour le moment, le jeu se déroule sur une unique carte pré-définie,
non générée aléatoirement.
Une carte est un rectangle composé de tuiles_.
La carte est chargée depuis sa représentation ASCII dans un fichier texte.
Au lancement du jeu, une quantité aléatoire d'entités_ sont générées et placées
aléatoirement sur la carte.
Tuiles
------
Une tuile représente une case du jeu, avec ses différentes propriétés physiques.
On compte actuellement 3 types de tuiles :
Vide
~~~~
Le vide est représenté par un espace vide quelque que soit le `pack de textures`_
utilisé. Cette tuile n'est utilisée que pour délimiter les bords de la carte,
aucune entité ne peut se trouver sur cette tuile.
Sol
~~~
Le sol représente les emplacements où les entités peuvent se déplacer librement.
Il est représenté par un point ``.`` dans le `pack de textures`_ ASCII et par
deux caractères rectangulaires blancs ``██`` dans le `pack de textures`_
écureuil.
Mur
~~~
Les murs délimitent les salles du donjon. Personne ne peut les traverser.
Ils sont représentés par un dièse ``#`` dans le `pack de textures`_ ASCII et
par une brique carrée ``🧱`` dans le `pack de textures`_ écureuil.

2
docs/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
sphinx
sphinx-rtd-theme

24
docs/rules.rst Normal file
View File

@ -0,0 +1,24 @@
Règles du jeu
=============
.. _carte: map.html
.. _objets: entities/items.html
.. _monstres: entities/monsters.html
.. _entités: entities/index.html
Dans `Squirrel Game`, le joueur incarne un écureuil coincé dans un donjon,
prêt à tout pour s'en sortir. Sa vision de rongeur lui permet d'observer
l'intégralité de la carte_, et à l'aide d'objets_, il va pouvoir affronter
les monstres_ présents dans le donjon et gagner en expérience et en force.
Le jeu fonctionne par niveau. À chaque niveau ``n`` du joueur, entre ``3n`` et
``7n`` entités apparaissent aléatoirement sur la carte.
En tuant des ennemis, ce qu'il parvient à faire en fonçant directement sur eux
ayant mangé trop de noisettes (ou étant armé d'un couteau), l'écureuil va
pouvoir gagner en expérience et au fur et à mesure qu'il monte de niveau,
a force augmentera.
Arriverez-vous à sauver ce malheureux petit écureuil perdu ?
Bon courage sachant que le jeu est sans fin ...

4
docs/settings.rst Normal file
View File

@ -0,0 +1,4 @@
Paramètres
==========
Pas encore documenté.

12
docs/tests.rst Normal file
View File

@ -0,0 +1,12 @@
Exécution des tests
===================
.. note::
La documentation va être revue ici.
Les tests sont gérés par ``pytest`` dans le module ``squirrelbattle.tests``.
``tox`` est un outil permettant de configurer l'exécution des tests. Ainsi, après
installation de tox dans votre environnement virtuel via ``pip install tox``,
il vous suffit d'exécuter ``tox -e py3`` pour lancer les tests et ``tox -e linters``
pour vérifier la syntaxe du code.

65
docs/texture-pack.rst Normal file
View File

@ -0,0 +1,65 @@
Pack de textures
================
.. _entité: entity/index.html
.. _tuile: map.html#tuiles
.. _tuiles: map.html#tuiles
.. _carte: map.html
.. _paramètres: settings.html
.. _Joueur: entities/player.html
.. _Hérisson: entities/monsters.html#herisson
.. _Cœur: entities/items.html#coeur
.. _Bombe: entities/items.html#bombe
.. _Lapin: entities/monsters.html#lapin
.. _Tigre: entities/monsters.html#tigre
.. _Nounours: entities/monsters.html#nounours
Chaque entité_ et chaque tuile_ de la carte_ est associé à un caractère pour
être affiché dans le terminal. Cependant, afin de pouvoir proposer plusieurs
expériences graphiques (notamment en fonction du support des émojis), différents
packs de textures sont proposés.
Il est possible de changer de pack dans les paramètres.
Les packs de textures peuvent influencer la taille que prennent les tuiles_,
en raison du fait que les émojis ne sont pas monospace.
Les packs de textures sont au nombre de deux :
Pack ASCII
----------
Chaque tuile fait un caractère de large.
* Tuiles
* Vide : *espace*
* Mur : ``#``
* Sol : ``.``
* Entités
* Joueur_ : ``@``
* Hérisson_ : ``*``
* Cœur_ : ````
* Bombe_ : ``o``
* Lapin_ : ``Y``
* Tigre_ : ``n``
* Nounours_ : ``8``
Pack Écureuil
-------------
Chaque tuile fait 2 caractères de large pour afficher les émojis proprement.
* Tuiles
* Vide : *espace*
* Mur : ``🧱``
* Sol : ``██``
* Entités
* Joueur_ : ``🐿``
* Hérisson_ : ``🦔``
* Cœur_ : ``💜``
* Bombe_ : ``💣``
* Lapin_ : ``🐇``
* Tigre_ : ``🐅``
* 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.

47
docs/troubleshooting.rst Normal file
View File

@ -0,0 +1,47 @@
Résolution d'erreurs
====================
Émojis
------
Le jeu s'exécutant en terminal, il est courant d'obtenir des problèmes d'affichage.
Sous Windows, les émojis s'affichent normalement correctement. Il suffit en
général d'installer les bons paquets de police.
Sous Arch Linux
^^^^^^^^^^^^^^^
Il est recommandé d'utiliser le terminal `xfce4-terminal`. Il suffit d'installer
le paquets de polices :
.. code:: bash
sudo pacman -Sy noto-fonts-emoji
Le jeu doit ensuite se lancer normalement sans action supplémentaire.
Sous Ubuntu/Debian
^^^^^^^^^^^^^^^^^^
À nouveau, le terminal `xfce4-terminal` est recommandé. Le paquet
`fonts-noto-color-emoji`.
Toutefois, 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
appliquer un patch qui permet d'afficher les émojis correctement dans son
terminal. Pour cela, il suffit de faire :
.. code:: bash
ln -s $PWD/debian/75-fix-squirrel-emojis.conf /etc/fonts/conf.avail/75-fix-squirrel-emojis.conf
ln -s /etc/fonts/conf.avail/75-fix-squirrel-emojis.conf /etc/fonts/conf.d/75-fix-squirrel-emojis.conf
Après redémarrage du terminal, l'écureuil devrait s'afficher correctement.
Pour supprimer le patch :
.. code:: bash
rm /etc/fonts/conf.d/75-fix-squirrel-emojis.conf
À noter que ce patch est inclus dans le paquet Debian.

View File

@ -1,15 +0,0 @@
from dungeonbattle.game import Game
from dungeonbattle.display.display_manager import DisplayManager
from dungeonbattle.term_manager import TermManager
class Bootstrap:
@staticmethod
def run_game():
with TermManager() as term_manager: # pragma: no cover
game = Game()
game.new_game()
display = DisplayManager(term_manager.screen, game)
game.display_actions = display.handle_display_action
game.run(term_manager.screen)

View File

@ -1,52 +0,0 @@
import curses
from typing import Any, Optional, Union
from dungeonbattle.display.texturepack import TexturePack
from dungeonbattle.tests.screen import FakePad
class Display:
x: int
y: int
width: int
height: int
pad: Any
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
self.screen = screen
self.pack = pack or TexturePack.get_pack("ascii")
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
return curses.newpad(height, width) if self.screen else FakePad()
def init_pair(self, number: int, foreground: int, background: int) -> None:
return curses.init_pair(number, foreground, background) \
if self.screen else None
def color_pair(self, number: int) -> int:
return curses.color_pair(number) if self.screen else 0
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
self.x = x
self.y = y
self.width = width
self.height = height
if hasattr(self, "pad") and resize_pad:
self.pad.resize(self.height - 1, self.width - 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
if len(args) == 4:
self.resize(*args, resize_pad)
self.display()
def display(self) -> None:
raise NotImplementedError
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42

View File

@ -1,69 +0,0 @@
import curses
from dungeonbattle.display.mapdisplay import MapDisplay
from dungeonbattle.display.statsdisplay import StatsDisplay
from dungeonbattle.display.menudisplay import MenuDisplay, MainMenuDisplay
from dungeonbattle.display.texturepack import TexturePack
from typing import Any
from dungeonbattle.game import Game, GameMode
from dungeonbattle.enums import DisplayActions
class DisplayManager:
def __init__(self, screen: Any, g: Game):
self.game = g
self.screen = screen
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay = MapDisplay(screen, pack)
self.statsdisplay = StatsDisplay(screen, pack)
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack)
self.settingsmenudisplay = MenuDisplay(screen, pack)
self.displays = [self.statsdisplay, self.mapdisplay,
self.mainmenudisplay, self.settingsmenudisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None:
if action == DisplayActions.REFRESH:
self.refresh()
elif action == DisplayActions.UPDATE:
self.update_game_components()
def update_game_components(self) -> None:
for d in self.displays:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
def refresh(self) -> None:
if self.game.state == GameMode.PLAY:
# The map pad has already the good size
self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols,
resize_pad=False)
self.statsdisplay.refresh(self.rows * 4 // 5, 0,
self.rows // 5, self.cols)
if self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
if self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1)
self.resize_window()
def resize_window(self) -> bool:
"""
If the window got resized, ensure that the screen size got updated.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows,
self.cols): # pragma: nocover
curses.resizeterm(y, x)
return True
return False
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python
from dungeonbattle.interfaces import Map
from .display import Display
class MapDisplay(Display):
def __init__(self, *args):
super().__init__(*args)
def update_map(self, m: Map) -> None:
self.map = m
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.pad.addstr(0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for e in self.map.entities:
self.pad.addstr(e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
pminrow, pmincol = y - deltay, x - deltax
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
deltay, deltax = self.height - deltay, self.width - deltax
smaxrow = self.map.height - (y + deltay) + self.height - 1
smaxrow = min(smaxrow, self.height - 1)
smaxcol = self.pack.tile_width * self.map.width - \
(x + deltax) + self.width - 1
smaxcol = min(smaxcol, self.width - 1)
pminrow = max(0, min(self.map.height, pminrow))
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
self.pad.clear()
self.update_pad()
self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)

View File

@ -1,84 +0,0 @@
from typing import List
from dungeonbattle.menus import Menu, MainMenu
from .display import Display
class MenuDisplay(Display):
position: int
def __init__(self, *args):
super().__init__(*args)
self.menubox = self.newpad(self.rows, self.cols)
def update_menu(self, menu: Menu) -> None:
self.menu = menu
self.trueheight = len(self.values)
self.truewidth = max([len(a) for a in self.values])
# Menu values are printed in pad
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i])
def update_pad(self) -> None:
for i in range(self.trueheight):
self.pad.addstr(i, 0, " " + self.values[i])
# set a marker on the selected line
self.pad.addstr(self.menu.position, 0, ">")
def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
else self.trueheight - self.height + 2 \
if self.height - 2 >= self.trueheight - self.menu.position else 0
# Menu box
self.menubox.addstr(0, 0, "" + "" * (self.width - 2) + "")
for i in range(1, self.height - 1):
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.pad.refresh(cornery, 0, self.y + 1, self.x + 2,
self.height - 2 + self.y,
self.width - 2 + self.x)
@property
def preferred_width(self) -> int:
return self.truewidth + 6
@property
def preferred_height(self) -> int:
return self.trueheight + 2
@property
def values(self) -> List[str]:
return [str(a) for a in self.menu.values]
class MainMenuDisplay(Display):
def __init__(self, menu: MainMenu, *args):
super().__init__(*args)
self.menu = menu
self.pad = self.newpad(self.rows, self.cols)
with open("resources/ascii_art.txt", "r") as file:
self.title = file.read().split("\n")
self.menudisplay = MenuDisplay(self.screen, self.pack)
self.menudisplay.update_menu(self.menu)
def display(self) -> None:
for i in range(len(self.title)):
self.pad.addstr(4 + i, self.width // 2
- len(self.title[0]) // 2 - 1, self.title[i])
self.pad.refresh(0, 0, self.y, self.x, self.height, self.width)
menuwidth = min(self.menudisplay.preferred_width, self.width)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
self.menudisplay.refresh(
menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth)

View File

@ -1,52 +0,0 @@
import curses
from .display import Display
from dungeonbattle.entities.player import Player
class StatsDisplay(Display):
player: Player
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
def update_player(self, p: Player) -> None:
self.player = p
def update_pad(self) -> None:
string = ""
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,
self.player.max_xp, self.player.health,
self.player.maxhealth)
for _ in range(self.width - len(string2) - 1):
string2 = string2 + " "
self.pad.addstr(1, 0, string2)
string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
for _ in range(self.width - len(string3) - 1):
string3 = string3 + " "
self.pad.addstr(2, 0, string3)
inventory_str = "Inventaire : " + "".join(
self.pack[item.name.upper()] for item in self.player.inventory)
self.pad.addstr(3, 0, inventory_str)
if self.player.dead:
self.pad.addstr(4, 0, "VOUS ÊTES MORT",
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
def display(self) -> None:
self.pad.clear()
self.update_pad()
self.pad.refresh(0, 0, self.y, self.x,
4 + self.y, self.width + self.x)

View File

@ -1,60 +0,0 @@
from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map
class Item(Entity):
held: bool
held_by: Optional["Player"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.held = False
def drop(self, y: int, x: int) -> None:
if self.held:
self.held_by.inventory.remove(self)
self.held = False
self.held_by = None
self.map.add_entity(self)
self.move(y, x)
def hold(self, player: "Player") -> None:
self.held = True
self.held_by = player
self.map.remove_entity(self)
player.inventory.append(self)
class Heart(Item):
name: str = "heart"
healing: int = 5
def hold(self, player: "Player") -> None:
"""
When holding a heart, heal the player and don't put item in inventory.
"""
player.health = min(player.maxhealth, player.health + self.healing)
self.map.remove_entity(self)
class Bomb(Item):
name: str = "bomb"
damage: int = 5
exploding: bool
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.exploding = False
def drop(self, x: int, y: int) -> None:
super().drop(x, y)
self.exploding = True
def act(self, m: Map) -> None:
if self.exploding:
for e in m.entities:
if abs(e.x - self.x) + abs(e.y - self.y) <= 1 and \
isinstance(e, FightingEntity):
e.take_damage(self, self.damage)

View File

@ -1,58 +0,0 @@
from random import choice
from .player import Player
from ..interfaces import FightingEntity, Map
class Monster(FightingEntity):
def act(self, m: Map) -> None:
"""
By default, a monster will move randomly where it is possible
And if a player is close to the monster, the monster run on the player.
"""
target = None
for entity in m.entities:
if self.distance_squared(entity) <= 25 and \
isinstance(entity, Player):
target = entity
break
# A Dijkstra algorithm has ran that targets the player.
# With that way, monsters can simply follow the path.
# If they can't move and they are already close to the player,
# They hit.
if target and (self.y, self.x) in target.paths:
# Move to target player
next_y, next_x = target.paths[(self.y, self.x)]
moved = self.check_move(next_y, next_x, True)
if not moved and self.distance_squared(target) <= 1:
self.hit(target)
else:
for _ in range(100):
if choice([self.move_up, self.move_down,
self.move_left, self.move_right])():
break
class Beaver(Monster):
name = "beaver"
maxhealth = 30
strength = 2
class Hedgehog(Monster):
name = "hedgehog"
maxhealth = 10
strength = 3
class Rabbit(Monster):
name = "rabbit"
maxhealth = 15
strength = 1
class TeddyBear(Monster):
name = "teddy_bear"
maxhealth = 50
strength = 0

View File

@ -1,102 +0,0 @@
from random import randint
from typing import Dict, Tuple
from ..interfaces import FightingEntity
class Player(FightingEntity):
name = "player"
maxhealth: int = 20
strength: int = 5
intelligence: int = 1
charisma: int = 1
dexterity: int = 1
constitution: int = 1
level: int = 1
current_xp: int = 0
max_xp: int = 10
inventory: list
paths: Dict[Tuple[int, int], Tuple[int, int]]
def __init__(self):
super().__init__()
self.inventory = list()
def move(self, y: int, x: int) -> None:
"""
When the player moves, move the camera of the map.
"""
super().move(y, x)
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
"""
while self.current_xp > self.max_xp:
self.level += 1
self.current_xp -= self.max_xp
self.max_xp = self.level * 10
self.health = self.maxhealth
# TODO Remove it, that's only fun
self.map.spawn_random_entities(randint(3 * self.level,
10 * self.level))
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
"""
self.current_xp += xp
self.level_up()
# noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool:
"""
If the player tries to move but a fighting entity is there,
the player fights this entity.
It rewards some XP if it is dead.
"""
# Don't move if we are dead
if self.dead:
return False
for entity in self.map.entities:
if entity.y == y and entity.x == x:
if entity.is_fighting_entity():
self.hit(entity)
if entity.dead:
self.add_xp(randint(3, 7))
return True
elif entity.is_item():
entity.hold(self)
return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths
for monsters to go to the player.
"""
queue = [(self.y, self.x)]
visited = []
distances = {(self.y, self.x): 0}
predecessors = {}
while queue:
y, x = queue.pop(0)
visited.append((y, x))
if distances[(y, x)] >= max_distance:
continue
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[y][x].can_walk() or \
(new_y, new_x) in visited or \
(new_y, new_x) in queue:
continue
predecessors[(new_y, new_x)] = (y, x)
distances[(new_y, new_x)] = distances[(y, x)] + 1
queue.append((new_y, new_x))
self.paths = predecessors

View File

@ -1,91 +0,0 @@
from random import randint
from typing import Any, Optional
from .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions
from .interfaces import Map
from .settings import Settings
from . import menus
from .mapgeneration import randomwalk
from typing import Callable
class Game:
map: Map
player: Player
# display_actions is a display interface set by the bootstrapper
display_actions: Callable[[DisplayActions], None]
def __init__(self) -> None:
"""
Init the game.
"""
self.state = GameMode.MAINMENU
self.main_menu = menus.MainMenu()
self.settings_menu = menus.SettingsMenu()
self.settings = Settings()
self.settings.load_settings()
self.settings.write_settings()
self.settings_menu.update_values(self.settings)
def new_game(self) -> None:
"""
Create a new game on the screen.
"""
self.map = randomwalk.Generator().run()
self.player = Player()
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
self.map.spawn_random_entities(randint(3, 10))
@staticmethod
def load_game(filename: str) -> None:
# TODO loading map from a file
raise NotImplementedError()
def run(self, screen: Any) -> None:
"""
Main infinite loop.
We wait for a player action, then we do what that should be done
when the given key got pressed.
"""
while True: # pragma no cover
screen.clear()
screen.refresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None:
"""
Indicates what should be done when the given key is pressed,
according to the current game state.
"""
if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key)
elif self.state == GameMode.MAINMENU:
self.main_menu.handle_key_pressed(key, self)
elif self.state == GameMode.SETTINGS:
self.settings_menu.handle_key_pressed(key, raw_key, self)
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
"""
In play mode, arrows or zqsd should move the main character.
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU

View File

@ -1,236 +0,0 @@
#!/usr/bin/env python
from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List
from dungeonbattle.display.texturepack import TexturePack
class Map:
"""
Object that represents a Map with its width, height
and the whole tiles, with their custom properties.
"""
width: int
height: int
start_y: int
start_x: int
tiles: List[List["Tile"]]
entities: List["Entity"]
# coordinates of the point that should be
# on the topleft corner of the screen
currentx: int
currenty: int
def __init__(self, width: int, height: int, tiles: list,
start_y: int, start_x: int):
self.width = width
self.height = height
self.start_y = start_y
self.start_x = start_x
self.tiles = tiles
self.entities = []
def add_entity(self, entity: "Entity") -> None:
"""
Register a new entity in the map.
"""
self.entities.append(entity)
entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
"""
self.entities.remove(entity)
def is_free(self, y: int, x: int) -> bool:
"""
Indicates that the case at the coordinates (y, x) is empty.
"""
return 0 <= y < self.height and 0 <= x < self.width and \
self.tiles[y][x].can_walk() and \
not any(entity.x == x and entity.y == y for entity in self.entities)
@staticmethod
def load(filename: str) -> "Map":
"""
Read a file that contains the content of a map, and build a Map object.
"""
with open(filename, "r") as f:
file = f.read()
return Map.load_from_string(file)
@staticmethod
def load_from_string(content: str) -> "Map":
"""
Load a map represented by its characters and build a Map object.
"""
lines = content.split("\n")
first_line = lines[0]
start_y, start_x = map(int, first_line.split(" "))
lines = [line for line in lines[1:] if line]
height = len(lines)
width = len(lines[0])
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return Map(width, height, tiles, start_y, start_x)
def draw_string(self, pack: TexturePack) -> str:
"""
Draw the current map as a string object that can be rendered
in the window.
"""
return "\n".join("".join(tile.char(pack) for tile in line)
for line in self.tiles)
def spawn_random_entities(self, count: int) -> None:
"""
Put randomly {count} hedgehogs on the map, where it is available.
"""
for _ in range(count):
y, x = 0, 0
while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
tile = self.tiles[y][x]
if tile.can_walk():
break
entity = choice(Entity.get_all_entity_classes())()
entity.move(y, x)
self.add_entity(entity)
def tick(self) -> None:
"""
Trigger all entity events.
"""
for entity in self.entities:
entity.act(self)
class Tile(Enum):
EMPTY = auto()
WALL = auto()
FLOOR = auto()
@classmethod
def from_ascii_char(cls, ch: str) -> "Tile":
for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch:
return tile
raise ValueError(ch)
def char(self, pack: TexturePack) -> str:
return getattr(pack, self.name)
def is_wall(self) -> bool:
return self == Tile.WALL
def can_walk(self) -> bool:
"""
Check if an entity (player or not) can move in this tile.
"""
return not self.is_wall() and self != Tile.EMPTY
class Entity:
y: int
x: int
name: str
map: Map
def __init__(self):
self.y = 0
self.x = 0
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool:
free = self.map.is_free(y, x)
if free and move_if_possible:
self.move(y, x)
return free
def move(self, y: int, x: int) -> bool:
self.y = y
self.x = x
return True
def move_up(self, force: bool = False) -> bool:
return self.move(self.y - 1, self.x) if force else \
self.check_move(self.y - 1, self.x, True)
def move_down(self, force: bool = False) -> bool:
return self.move(self.y + 1, self.x) if force else \
self.check_move(self.y + 1, self.x, True)
def move_left(self, force: bool = False) -> bool:
return self.move(self.y, self.x - 1) if force else \
self.check_move(self.y, self.x - 1, True)
def move_right(self, force: bool = False) -> bool:
return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True)
def act(self, m: Map) -> None:
"""
Define the action of the entity that is ran each tick.
By default, does nothing.
"""
pass
def distance_squared(self, other: "Entity") -> int:
"""
Get the square of the distance to another entity.
Useful to check distances since square root takes time.
"""
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
def distance(self, other: "Entity") -> float:
"""
Get the cartesian distance to another entity.
"""
return sqrt(self.distance_squared(other))
def is_fighting_entity(self) -> bool:
return isinstance(self, FightingEntity)
def is_item(self) -> bool:
from dungeonbattle.entities.items import Item
return isinstance(self, Item)
@staticmethod
def get_all_entity_classes():
from dungeonbattle.entities.items import Heart, Bomb
from dungeonbattle.entities.monsters import Beaver, Hedgehog, \
Rabbit, TeddyBear
return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear]
class FightingEntity(Entity):
maxhealth: int
health: int
strength: int
dead: bool
intelligence: int
charisma: int
dexterity: int
constitution: int
level: int
def __init__(self):
super().__init__()
self.health = self.maxhealth
self.dead = False
def hit(self, opponent: "FightingEntity") -> None:
opponent.take_damage(self, self.strength)
def take_damage(self, attacker: "Entity", amount: int) -> None:
self.health -= amount
if self.health <= 0:
self.die()
def die(self) -> None:
self.dead = True
self.map.remove_entity(self)

View File

@ -1,216 +0,0 @@
import os
import unittest
from dungeonbattle.bootstrap import Bootstrap
from dungeonbattle.display.display import Display
from dungeonbattle.display.display_manager import DisplayManager
from dungeonbattle.entities.player import Player
from dungeonbattle.game import Game, KeyValues, GameMode
from dungeonbattle.menus import MainMenuValues
from dungeonbattle.settings import Settings
class TestGame(unittest.TestCase):
def setUp(self) -> None:
"""
Setup game.
"""
self.game = Game()
self.game.new_game()
display = DisplayManager(None, self.game)
self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None:
self.assertRaises(NotImplementedError, Game.load_game, "game.save")
self.assertRaises(NotImplementedError, Display(None).display)
def test_bootstrap_fail(self) -> None:
"""
Ensure that the test can't play the game,
because there is no associated shell.
Yeah, that's only for coverage.
"""
self.assertRaises(Exception, Bootstrap.run_game)
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
def test_key_translation(self) -> None:
"""
Test key bindings.
"""
self.game.settings = Settings()
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_PRIMARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_SECONDARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_PRIMARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_SECONDARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_PRIMARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_SECONDARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_PRIMARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_ENTER, self.game.settings),
KeyValues.ENTER)
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
KeyValues.SPACE)
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
None)
def test_key_press(self) -> None:
"""
Press a key and see what is done.
"""
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.EXIT)
self.assertRaises(SystemExit, self.game.handle_key_pressed,
KeyValues.ENTER)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.PLAY)
# Kill entities
for entity in self.game.map.entities.copy():
if not isinstance(entity, Player):
self.game.map.remove_entity(entity)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.DOWN)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y + 1)
self.assertEqual(new_x, x)
y, x = new_y, new_x
self.game.handle_key_pressed(KeyValues.RIGHT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x + 1)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.UP)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y - 1)
self.assertEqual(new_x, x)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.LEFT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x - 1)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_settings_menu(self) -> None:
"""
Ensure that the settings menu is working properly.
"""
self.game.settings = Settings()
# Open settings menu
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
# Define the "move up" key to 'w'
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'w')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
# Navigate to "move left"
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.UP)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
# Define the "move up" key to 'a'
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
# Can't used a mapped key
self.game.handle_key_pressed(None, 's')
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'a')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack"
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)
# Change texture pack
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "squirrel")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
# 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.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_dead_screen(self) -> None:
"""
Kill player and render dead screen.
"""
self.game.state = GameMode.PLAY
# Kill player
self.game.player.take_damage(self.game.player,
self.game.player.health + 2)
y, x = self.game.player.y, self.game.player.x
for key in [KeyValues.UP, KeyValues.DOWN,
KeyValues.LEFT, KeyValues.RIGHT]:
self.game.handle_key_pressed(key)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x)

View File

@ -1,24 +0,0 @@
import unittest
from dungeonbattle.menus import ArbitraryMenu, MainMenu, MainMenuValues
class TestMenus(unittest.TestCase):
def test_scroll_menu(self) -> None:
"""
Test to scroll the menu.
"""
arbitrary_menu = ArbitraryMenu([])
self.assertEqual(arbitrary_menu.position, 0)
main_menu = MainMenu()
self.assertEqual(main_menu.position, 0)
self.assertEqual(main_menu.validate(), MainMenuValues.START)
main_menu.go_up()
self.assertEqual(main_menu.validate(), MainMenuValues.START)
main_menu.go_down()
self.assertEqual(main_menu.validate(), MainMenuValues.SETTINGS)
main_menu.go_down()
self.assertEqual(main_menu.validate(), MainMenuValues.EXIT)
main_menu.go_down()
self.assertEqual(main_menu.validate(), MainMenuValues.EXIT)

26
main.py
View File

@ -1,5 +1,25 @@
#!/usr/bin/env python #!/usr/bin/env python3
from dungeonbattle.bootstrap import Bootstrap
# 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.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()

55
setup.py Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
# 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
with open("README.md", "r") as f:
long_description = f.read()
# Compile messages
for language in ["de", "es", "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",
]
}
)

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

@ -0,0 +1,24 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.game import Game
from squirrelbattle.display.display_manager import DisplayManager
from squirrelbattle.term_manager import TermManager
class Bootstrap:
"""
The bootstrap object is used to bootstrap the game so that it starts
properly.
(It was initially created to avoid circular imports between the Game and
Display classes)
"""
@staticmethod
def run_game():
with TermManager() as term_manager: # pragma: no cover
game = Game()
game.new_game()
display = DisplayManager(term_manager.screen, game)
game.display_actions = display.handle_display_action
game.run(term_manager.screen)

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

@ -0,0 +1,160 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import Any, Optional, Union
from squirrelbattle.display.texturepack import TexturePack
from squirrelbattle.tests.screen import FakePad
class Display:
x: int
y: int
width: int
height: int
pad: Any
def __init__(self, screen: Any, pack: Optional[TexturePack] = None):
self.screen = screen
self.pack = pack or TexturePack.get_pack("ascii")
def newpad(self, height: int, width: int) -> Union[FakePad, Any]:
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:
return curses.init_pair(number, foreground, background) \
if self.screen else None
def color_pair(self, number: int) -> int:
return curses.color_pair(number) if self.screen else 0
def resize(self, y: int, x: int, height: int, width: int,
resize_pad: bool = True) -> None:
self.x = x
self.y = y
self.width = width
self.height = height
if hasattr(self, "pad") and resize_pad and \
self.height >= 0 and self.width >= 0:
self.pad.resize(self.height + 1, self.width + 1)
def refresh(self, *args, resize_pad: bool = True) -> None:
if len(args) == 4:
self.resize(*args, resize_pad)
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:
raise NotImplementedError
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
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

@ -0,0 +1,107 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from squirrelbattle.display.display import VerticalSplit, HorizontalSplit
from squirrelbattle.display.mapdisplay import MapDisplay
from squirrelbattle.display.messagedisplay import MessageDisplay
from squirrelbattle.display.statsdisplay import StatsDisplay
from squirrelbattle.display.menudisplay import MainMenuDisplay, \
InventoryDisplay, SettingsMenuDisplay
from squirrelbattle.display.logsdisplay import LogsDisplay
from squirrelbattle.display.texturepack import TexturePack
from typing import Any
from squirrelbattle.game import Game, GameMode
from squirrelbattle.enums import DisplayActions
class DisplayManager:
def __init__(self, screen: Any, g: Game):
self.game = g
self.screen = screen
pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay = MapDisplay(screen, pack)
self.statsdisplay = StatsDisplay(screen, pack)
self.logsdisplay = LogsDisplay(screen, pack)
self.inventorydisplay = InventoryDisplay(screen, pack)
self.mainmenudisplay = MainMenuDisplay(self.game.main_menu,
screen, pack)
self.settingsmenudisplay = SettingsMenuDisplay(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.mainmenudisplay, self.settingsmenudisplay,
self.logsdisplay, self.messagedisplay]
self.update_game_components()
def handle_display_action(self, action: DisplayActions) -> None:
if action == DisplayActions.REFRESH:
self.refresh()
elif action == DisplayActions.UPDATE:
self.update_game_components()
def update_game_components(self) -> None:
for d in self.displays:
d.pack = TexturePack.get_pack(self.game.settings.TEXTURE_PACK)
self.mapdisplay.update_map(self.game.map)
self.statsdisplay.update_player(self.game.player)
self.inventorydisplay.update_menu(self.game.inventory_menu)
self.settingsmenudisplay.update_menu(self.game.settings_menu)
self.logsdisplay.update_logs(self.game.logs)
self.messagedisplay.update_message(self.game.message)
def refresh(self) -> None:
if self.game.state == GameMode.PLAY \
or self.game.state == GameMode.INVENTORY:
# The map pad has already the good size
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)
self.statsdisplay.refresh(0, self.cols * 4 // 5 + 1,
self.rows, self.cols // 5 - 1)
self.logsdisplay.refresh(self.rows * 4 // 5 + 1, 0,
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.INVENTORY:
self.inventorydisplay.refresh(self.rows // 10,
self.cols // 2,
8 * self.rows // 10,
2 * self.cols // 5)
elif self.game.state == GameMode.MAINMENU:
self.mainmenudisplay.refresh(0, 0, self.rows, self.cols)
elif self.game.state == GameMode.SETTINGS:
self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols)
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()
def resize_window(self) -> bool:
"""
If the window got resized, ensure that the screen size got updated.
"""
y, x = self.screen.getmaxyx() if self.screen else (0, 0)
if self.screen and curses.is_term_resized(self.rows,
self.cols): # pragma: nocover
curses.resizeterm(y, x)
return True
return False
@property
def rows(self) -> int:
return curses.LINES if self.screen else 42
@property
def cols(self) -> int:
return curses.COLS if self.screen else 42

View File

@ -0,0 +1,25 @@
# 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.interfaces import Logs
class LogsDisplay(Display):
def __init__(self, *args) -> None:
super().__init__(*args)
self.pad = self.newpad(self.rows, self.cols)
def update_logs(self, logs: Logs) -> None:
self.logs = logs
def display(self) -> None:
messages = self.logs.messages[-self.height:]
messages = messages[::-1]
self.pad.erase()
for i in range(min(self.height, len(messages))):
self.addstr(self.pad, self.height - i - 1, self.x,
messages[i][:self.width])
self.refresh_pad(self.pad, 0, 0, self.y, self.x,
self.y + self.height - 1, self.x + self.width - 1)

View File

@ -0,0 +1,71 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from squirrelbattle.interfaces import Map
from .display import Display
class MapDisplay(Display):
def __init__(self, *args):
super().__init__(*args)
def update_map(self, m: Map) -> None:
self.map = m
self.pad = self.newpad(m.height, self.pack.tile_width * m.width + 1)
def update_pad(self) -> None:
self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color)
self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color)
self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack),
self.color_pair(1))
for e in self.map.entities:
self.addstr(self.pad, e.y, self.pack.tile_width * e.x,
self.pack[e.name.upper()], self.color_pair(2))
# Display Path map for deubg purposes
# from squirrelbattle.entities.player import Player
# players = [ p for p in self.map.entities if isinstance(p,Player) ]
# player = players[0] if len(players) > 0 else None
# if player:
# for x in range(self.map.width):
# for y in range(self.map.height):
# if (y,x) in player.paths:
# deltay, deltax = (y - player.paths[(y, x)][0],
# x - player.paths[(y, x)][1])
# if (deltay, deltax) == (-1, 0):
# character = '↓'
# elif (deltay, deltax) == (1, 0):
# character = '↑'
# elif (deltay, deltax) == (0, -1):
# character = '→'
# else:
# character = '←'
# self.addstr(self.pad, y, self.pack.tile_width * x,
# character, self.color_pair(1))
def display(self) -> None:
y, x = self.map.currenty, self.pack.tile_width * self.map.currentx
deltay, deltax = (self.height // 2) + 1, (self.width // 2) + 1
pminrow, pmincol = y - deltay, x - deltax
sminrow, smincol = max(-pminrow, 0), max(-pmincol, 0)
deltay, deltax = self.height - deltay, self.width - deltax
smaxrow = self.map.height - (y + deltay) + self.height - 1
smaxrow = min(smaxrow, self.height - 1)
smaxcol = self.pack.tile_width * self.map.width - \
(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)
pminrow = max(0, min(self.map.height, pminrow))
pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol))
self.pad.erase()
self.update_pad()
self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow,
smaxcol)

View File

@ -0,0 +1,120 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from typing import List
from squirrelbattle.menus import Menu, MainMenu
from .display import Display, Box
from ..resources import ResourceManager
from ..translations import gettext as _
class MenuDisplay(Display):
position: int
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.menubox = Box(*args, **kwargs)
def update_menu(self, menu: Menu) -> None:
self.menu = menu
# Menu values are printed in pad
self.pad = self.newpad(self.trueheight, self.truewidth + 2)
def update_pad(self) -> None:
for i in range(self.trueheight):
self.addstr(self.pad, i, 0, " " + self.values[i])
# set a marker on the selected line
self.addstr(self.pad, self.menu.position, 0, ">")
def display(self) -> None:
cornery = 0 if self.height - 2 >= self.menu.position - 1 \
else self.trueheight - self.height + 2 \
if self.height - 2 >= self.trueheight - self.menu.position else 0
# Menu box
self.menubox.refresh(self.y, self.x, self.height, self.width)
self.pad.erase()
self.update_pad()
self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2,
self.height - 2 + self.y,
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
def preferred_width(self) -> int:
return self.truewidth + 6
@property
def preferred_height(self) -> int:
return self.trueheight + 2
@property
def values(self) -> List[str]:
return [str(a) for a in self.menu.values]
class SettingsMenuDisplay(MenuDisplay):
@property
def values(self) -> List[str]:
return [_(a[1][1]) + (" : "
+ ("?" if self.menu.waiting_for_key
and a == self.menu.validate() else a[1][0]
.replace("\n", "\\n"))
if a[1][0] else "") for a in self.menu.values]
class MainMenuDisplay(Display):
def __init__(self, menu: MainMenu, *args):
super().__init__(*args)
self.menu = menu
with open(ResourceManager.get_asset_path("ascii_art.txt"), "r") as file:
self.title = file.read().split("\n")
self.pad = self.newpad(max(self.rows, len(self.title) + 30),
max(len(self.title[0]) + 5, self.cols))
self.menudisplay = MenuDisplay(self.screen, self.pack)
self.menudisplay.update_menu(self.menu)
def display(self) -> None:
for i in range(len(self.title)):
self.addstr(self.pad, 4 + i, max(self.width // 2
- len(self.title[0]) // 2 - 1, 0), self.title[i])
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)
menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1
self.menudisplay.refresh(
menuy, menux, min(self.menudisplay.preferred_height,
self.height - menuy), menuwidth)
class InventoryDisplay(MenuDisplay):
def update_pad(self) -> None:
message = _("== INVENTORY ==")
self.addstr(self.pad, 0, (self.width - len(message)) // 2, message,
curses.A_BOLD | curses.A_ITALIC)
for i, item in enumerate(self.menu.values):
rep = self.pack[item.name.upper()]
selection = f"[{rep}]" if i == self.menu.position else f" {rep} "
self.addstr(self.pad, 2 + i, 0, selection
+ " " + item.translated_name.capitalize())
@property
def truewidth(self) -> int:
return max(1, self.height if hasattr(self, "height") else 10)
@property
def trueheight(self) -> int:
return 2 + super().trueheight

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

@ -0,0 +1,58 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import curses
from ..entities.player import Player
from ..translations import gettext as _
from .display import Display
class StatsDisplay(Display):
player: Player
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pad = self.newpad(self.rows, self.cols)
self.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
def update_player(self, p: Player) -> None:
self.player = p
def update_pad(self) -> None:
string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\
.format(self.player.level, self.player.current_xp,
self.player.max_xp, self.player.health,
self.player.maxhealth)
self.addstr(self.pad, 0, 0, string2)
string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\
.format(self.player.strength,
self.player.intelligence, self.player.charisma,
self.player.dexterity, self.player.constitution)
self.addstr(self.pad, 3, 0, string3)
inventory_str = _("Inventory:") + " "
# Stack items by type instead of displaying each item
item_types = [item.name for item in self.player.inventory]
item_types.sort(key=item_types.count, reverse=True)
printed_items = []
for item in item_types:
if item in printed_items:
continue
count = item_types.count(item)
inventory_str += self.pack[item.upper()]
if count > 1:
inventory_str += f"x{count} "
printed_items.append(item)
self.addstr(self.pad, 8, 0, inventory_str)
if self.player.dead:
self.addstr(self.pad, 10, 0, _("YOU ARE DEAD"),
curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT
| self.color_pair(3))
def display(self) -> None:
self.pad.erase()
self.update_pad()
self.refresh_pad(self.pad, 0, 0, self.y, 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,8 +54,9 @@ TexturePack.ASCII_PACK = TexturePack(
HEART='', HEART='',
BOMB='o', BOMB='o',
RABBIT='Y', RABBIT='Y',
BEAVER='_', TIGER='n',
TEDDY_BEAR='8', TEDDY_BEAR='8',
BODY_SNATCH_POTION='S',
) )
TexturePack.SQUIRREL_PACK = TexturePack( TexturePack.SQUIRREL_PACK = TexturePack(
@ -65,11 +69,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='🧸',
BODY_SNATCH_POTION='🔀',
) )

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

@ -0,0 +1,178 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import choice, randint
from typing import Optional
from .player import Player
from ..interfaces import Entity, FightingEntity, Map
from ..translations import gettext as _
class Item(Entity):
"""
A class for items
"""
held: bool
held_by: Optional[Player]
def __init__(self, held: bool = False, held_by: Optional[Player] = None,
*args, **kwargs):
super().__init__(*args, **kwargs)
self.held = held
self.held_by = held_by
def drop(self) -> None:
"""
The item is dropped from the inventory onto the floor
"""
if self.held:
self.held_by.inventory.remove(self)
self.map.add_entity(self)
self.move(self.held_by.y, self.held_by.x)
self.held = False
self.held_by = None
def use(self) -> None:
"""
Indicates what should be done when the item is used.
"""
def equip(self) -> None:
"""
Indicates what should be done when the item is equipped.
"""
def hold(self, player: "Player") -> None:
"""
The item is taken from the floor and put into the inventory
"""
self.held = True
self.held_by = player
self.map.remove_entity(self)
player.inventory.append(self)
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
d["held"] = self.held
return d
class Heart(Item):
"""
A heart item to return health to the player
"""
healing: int
def __init__(self, name: str = "heart", healing: int = 5, *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
self.healing = healing
def hold(self, player: "Player") -> None:
"""
When holding a heart, heal the player and don't put item in inventory.
"""
player.health = min(player.maxhealth, player.health + self.healing)
self.map.remove_entity(self)
def save_state(self) -> dict:
"""
Saves the state of the header into a dictionary
"""
d = super().save_state()
d["healing"] = self.healing
return d
class Bomb(Item):
"""
A bomb item intended to deal damage to enemies at long range
"""
damage: int = 5
exploding: bool
owner: Optional["Player"]
tick: int
def __init__(self, name: str = "bomb", damage: int = 5,
exploding: bool = False, *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
self.damage = damage
self.exploding = exploding
self.tick = 4
self.owner = None
def use(self) -> None:
"""
When the bomb is used, throw it and explodes it.
"""
if self.held:
self.owner = self.held_by
super().drop()
self.exploding = True
def act(self, m: Map) -> None:
"""
Special exploding action of the bomb
"""
if self.exploding:
if self.tick > 0:
# The bomb will explode in <tick> moves
self.tick -= 1
else:
# The bomb is exploding.
# Each entity that is close to the bomb takes damages.
# The player earn XP if the entity was killed.
log_message = _("Bomb is exploding.")
for e in m.entities.copy():
if abs(e.x - self.x) + abs(e.y - self.y) <= 3 and \
isinstance(e, FightingEntity):
log_message += " " + e.take_damage(self, self.damage)
if e.dead:
self.owner.add_xp(randint(3, 7))
m.logs.add_message(log_message)
m.entities.remove(self)
def save_state(self) -> dict:
"""
Saves the state of the bomb into a dictionary
"""
d = super().save_state()
d["exploding"] = self.exploding
d["damage"] = self.damage
return d
class BodySnatchPotion(Item):
"""
The body-snatch potion allows to exchange all characteristics with a random
other entity.
"""
def __init__(self, name: str = "body_snatch_potion", *args, **kwargs):
super().__init__(name=name, *args, **kwargs)
def use(self) -> None:
"""
Find a valid random entity, then exchange characteristics.
"""
valid_entities = self.held_by.map.find_entities(FightingEntity)
valid_entities.remove(self.held_by)
entity = choice(valid_entities)
entity_state = entity.save_state()
player_state = self.held_by.save_state()
self.held_by.__dict__.update(entity_state)
entity.__dict__.update(player_state)
self.held_by.map.currenty, self.held_by.map.currentx = self.held_by.y,\
self.held_by.x
self.held_by.map.logs.add_message(
_("{player} exchanged its body with {entity}.").format(
player=self.held_by.translated_name.capitalize(),
entity=entity.translated_name))
self.held_by.recalculate_paths()
self.held_by.inventory.remove(self)

View File

@ -0,0 +1,102 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
from .player import Player
from ..interfaces import FightingEntity, Map
class Monster(FightingEntity):
"""
The class for all monsters in the dungeon.
A monster must override this class, and the parameters are given
in the __init__ function.
An example of the specification of a monster that has a strength of 4
and 20 max HP:
class MyMonster(Monster):
def __init__(self, strength: int = 4, maxhealth: int = 20,
*args, **kwargs) -> None:
super().__init__(name="my_monster", strength=strength,
maxhealth=maxhealth, *args, **kwargs)
With that way, attributes can be overwritten when the entity got created.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def act(self, m: Map) -> None:
"""
By default, a monster will move randomly where it is possible
And if a player is close to the monster, the monster run on the player.
"""
target = None
for entity in m.entities:
if self.distance_squared(entity) <= 25 and \
isinstance(entity, Player):
target = entity
break
# A Dijkstra algorithm has ran that targets the player.
# With that way, monsters can simply follow the path.
# If they can't move and they are already close to the player,
# They hit.
if target and (self.y, self.x) in target.paths:
# Move to target player by choosing the best avaliable path
for next_y, next_x in target.paths[(self.y, self.x)]:
moved = self.check_move(next_y, next_x, True)
if moved:
break
if self.distance_squared(target) <= 1:
self.map.logs.add_message(self.hit(target))
break
else:
# Move in a random direction
# If the direction is not available, try another one
moves = [self.move_up, self.move_down,
self.move_left, self.move_right]
shuffle(moves)
for move in moves:
if move():
break
class Tiger(Monster):
"""
A tiger monster
"""
def __init__(self, name: str = "tiger", strength: int = 2,
maxhealth: int = 20, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class Hedgehog(Monster):
"""
A really mean hedgehog monster
"""
def __init__(self, name: str = "hedgehog", strength: int = 3,
maxhealth: int = 10, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class Rabbit(Monster):
"""
A rabbit monster
"""
def __init__(self, name: str = "rabbit", strength: int = 1,
maxhealth: int = 15, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)
class TeddyBear(Monster):
"""
A cute teddybear monster
"""
def __init__(self, name: str = "teddy_bear", strength: int = 0,
maxhealth: int = 50, *args, **kwargs) -> None:
super().__init__(name=name, strength=strength,
maxhealth=maxhealth, *args, **kwargs)

View File

@ -0,0 +1,153 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from functools import reduce
from queue import PriorityQueue
from random import randint
from typing import Dict, Tuple
from ..interfaces import FightingEntity
class Player(FightingEntity):
"""
The class of the player
"""
current_xp: int = 0
max_xp: int = 10
inventory: list
paths: Dict[Tuple[int, int], Tuple[int, int]]
def __init__(self, name: str = "player", maxhealth: int = 20,
strength: int = 5, intelligence: int = 1, charisma: int = 1,
dexterity: int = 1, constitution: int = 1, level: int = 1,
current_xp: int = 0, max_xp: int = 10, inventory: list = None,
*args, **kwargs) \
-> None:
super().__init__(name=name, maxhealth=maxhealth, strength=strength,
intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution,
level=level, *args, **kwargs)
self.current_xp = current_xp
self.max_xp = max_xp
self.inventory = inventory if inventory else list()
for i in range(len(self.inventory)):
if isinstance(self.inventory[i], dict):
entity_classes = self.get_all_entity_classes_in_a_dict()
item_class = entity_classes[self.inventory[i]["type"]]
self.inventory[i] = item_class(**self.inventory[i])
self.paths = dict()
def move(self, y: int, x: int) -> None:
"""
Moves the view of the map (the point on which the camera is centered)
according to the moves of the player.
"""
super().move(y, x)
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
"""
while self.current_xp > self.max_xp:
self.level += 1
self.current_xp -= self.max_xp
self.max_xp = self.level * 10
self.health = self.maxhealth
self.strength = self.strength + 1
# TODO Remove it, that's only fun
self.map.spawn_random_entities(randint(3 * self.level,
10 * self.level))
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
"""
self.current_xp += xp
self.level_up()
# noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool:
"""
If the player tries to move but a fighting entity is there,
the player fights this entity.
If the entity dies, the player is rewarded with some XP
"""
# Don't move if we are dead
if self.dead:
return False
for entity in self.map.entities:
if entity.y == y and entity.x == x:
if entity.is_fighting_entity():
self.map.logs.add_message(self.hit(entity))
if entity.dead:
self.add_xp(randint(3, 7))
return True
elif entity.is_item():
entity.hold(self)
return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths for monsters to go to
the player. Actually, the paths are computed for each tile adjacent to
the player then for each step the monsters use the best path avaliable.
"""
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
d["current_xp"] = self.current_xp
d["max_xp"] = self.max_xp
d["inventory"] = [item.save_state() for item in self.inventory]
return d

View File

@ -1,15 +1,27 @@
# 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
from dungeonbattle.settings import Settings from squirrelbattle.settings import Settings
# This file contains a few useful enumeration classes used elsewhere in the code
class DisplayActions(Enum): class DisplayActions(Enum):
"""
Display actions options for the callable displayaction Game uses
It just calls the same action on the display object displayaction refers to.
"""
REFRESH = auto() REFRESH = auto()
UPDATE = auto() UPDATE = auto()
class GameMode(Enum): class GameMode(Enum):
"""
Game mode options
"""
MAINMENU = auto() MAINMENU = auto()
PLAY = auto() PLAY = auto()
SETTINGS = auto() SETTINGS = auto()
@ -17,11 +29,18 @@ class GameMode(Enum):
class KeyValues(Enum): class KeyValues(Enum):
"""
Key values options used in the game
"""
UP = auto() UP = auto()
DOWN = auto() DOWN = auto()
LEFT = auto() LEFT = auto()
RIGHT = auto() RIGHT = auto()
ENTER = auto() ENTER = auto()
INVENTORY = auto()
USE = auto()
EQUIP = auto()
DROP = auto()
SPACE = auto() SPACE = auto()
@staticmethod @staticmethod
@ -43,6 +62,14 @@ class KeyValues(Enum):
return KeyValues.UP return KeyValues.UP
elif key == settings.KEY_ENTER: elif key == settings.KEY_ENTER:
return KeyValues.ENTER return KeyValues.ENTER
elif key == settings.KEY_INVENTORY:
return KeyValues.INVENTORY
elif key == settings.KEY_USE:
return KeyValues.USE
elif key == settings.KEY_EQUIP:
return KeyValues.EQUIP
elif key == settings.KEY_DROP:
return KeyValues.DROP
elif key == ' ': elif key == ' ':
return KeyValues.SPACE return KeyValues.SPACE
return None return None

216
squirrelbattle/game.py Normal file
View File

@ -0,0 +1,216 @@
# 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 typing import Any, Optional
import json
import os
import sys
from .entities.player import Player
from .enums import GameMode, KeyValues, DisplayActions
from .interfaces import Map, Logs
from .resources import ResourceManager
from .settings import Settings
from . import menus
from .translations import gettext as _, Translator
from typing import Callable
class Game:
"""
The game object controls all actions in the game.
"""
map: Map
player: Player
# display_actions is a display interface set by the bootstrapper
display_actions: Callable[[DisplayActions], None]
def __init__(self) -> None:
"""
Init the game.
"""
self.state = GameMode.MAINMENU
self.settings = Settings()
self.settings.load_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.inventory_menu = menus.InventoryMenu()
self.logs = Logs()
self.message = None
def new_game(self) -> None:
"""
Create a new game on the screen.
"""
# TODO generate a new map procedurally
self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.map.logs = self.logs
self.logs.clear()
self.player = Player()
self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x)
self.map.spawn_random_entities(randint(3, 10))
self.inventory_menu.update_player(self.player)
def run(self, screen: Any) -> None:
"""
Main infinite loop.
We wait for the player's action, then we do what that should be done
when the given key gets pressed.
"""
while True: # pragma no cover
screen.erase()
screen.refresh()
self.display_actions(DisplayActions.REFRESH)
key = screen.getkey()
self.handle_key_pressed(
KeyValues.translate_key(key, self.settings), key)
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\
-> None:
"""
Indicates what should be done when the given key is pressed,
according to the current game state.
"""
if self.message:
self.message = None
self.display_actions(DisplayActions.REFRESH)
return
if self.state == GameMode.PLAY:
self.handle_key_pressed_play(key)
elif self.state == GameMode.INVENTORY:
self.handle_key_pressed_inventory(key)
elif self.state == GameMode.MAINMENU:
self.handle_key_pressed_main_menu(key)
elif self.state == GameMode.SETTINGS:
self.settings_menu.handle_key_pressed(key, raw_key, self)
self.display_actions(DisplayActions.REFRESH)
def handle_key_pressed_play(self, key: KeyValues) -> None:
"""
In play mode, arrows or zqsd move the main character.
"""
if key == KeyValues.UP:
if self.player.move_up():
self.map.tick()
elif key == KeyValues.DOWN:
if self.player.move_down():
self.map.tick()
elif key == KeyValues.LEFT:
if self.player.move_left():
self.map.tick()
elif key == KeyValues.RIGHT:
if self.player.move_right():
self.map.tick()
elif key == KeyValues.INVENTORY:
self.state = GameMode.INVENTORY
elif key == KeyValues.SPACE:
self.state = GameMode.MAINMENU
def handle_key_pressed_inventory(self, key: KeyValues) -> None:
"""
In the inventory menu, we can interact with items or close the menu.
"""
if key == KeyValues.SPACE or key == KeyValues.INVENTORY:
self.state = GameMode.PLAY
elif key == KeyValues.UP:
self.inventory_menu.go_up()
elif key == KeyValues.DOWN:
self.inventory_menu.go_down()
if self.inventory_menu.values and not self.player.dead:
if key == KeyValues.USE:
self.inventory_menu.validate().use()
elif key == KeyValues.EQUIP:
self.inventory_menu.validate().equip()
elif key == KeyValues.DROP:
self.inventory_menu.validate().drop()
# Ensure that the cursor has a good position
self.inventory_menu.position = min(self.inventory_menu.position,
len(self.inventory_menu.values)
- 1)
def handle_key_pressed_main_menu(self, key: KeyValues) -> None:
"""
In the main menu, we can navigate through options.
"""
if key == KeyValues.DOWN:
self.main_menu.go_down()
if key == KeyValues.UP:
self.main_menu.go_up()
if key == KeyValues.ENTER:
option = self.main_menu.validate()
if option == menus.MainMenuValues.START:
self.new_game()
self.display_actions(DisplayActions.UPDATE)
self.state = GameMode.PLAY
if option == menus.MainMenuValues.RESUME:
self.state = GameMode.PLAY
elif option == menus.MainMenuValues.SAVE:
self.save_game()
elif option == menus.MainMenuValues.LOAD:
self.load_game()
elif option == menus.MainMenuValues.SETTINGS:
self.state = GameMode.SETTINGS
elif option == menus.MainMenuValues.EXIT:
sys.exit(0)
def save_state(self) -> dict:
"""
Saves the game to a dictionary
"""
return self.map.save_state()
def load_state(self, d: dict) -> None:
"""
Loads the game from a dictionary
"""
try:
self.map.load_state(d)
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)
def load_game(self) -> None:
"""
Loads the game from a file
"""
file_path = ResourceManager.get_config_path("save.json")
if os.path.isfile(file_path):
with open(file_path, "r") as f:
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:
"""
Saves the game to a file
"""
with open(ResourceManager.get_config_path("save.json"), "w") as f:
f.write(json.dumps(self.save_state()))

View File

@ -0,0 +1,438 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum, auto
from math import sqrt
from random import choice, randint
from typing import List, Optional
from .display.texturepack import TexturePack
from .translations import gettext as _
class Logs:
"""
The logs object stores the messages to display. It is encapsulating a list
of such messages, to allow multiple pointers to keep track of it even if
the list was to be reassigned.
"""
def __init__(self) -> None:
self.messages = []
def add_message(self, msg: str) -> None:
self.messages.append(msg)
def add_messages(self, msg: List[str]) -> None:
self.messages += msg
def clear(self) -> None:
self.messages = []
class Map:
"""
Object that represents a Map with its width, height
and tiles, that have their custom properties.
"""
width: int
height: int
start_y: int
start_x: int
tiles: List[List["Tile"]]
entities: List["Entity"]
logs: Logs
# coordinates of the point that should be
# on the topleft corner of the screen
currentx: int
currenty: int
def __init__(self, width: int, height: int, tiles: list,
start_y: int, start_x: int):
self.width = width
self.height = height
self.start_y = start_y
self.start_x = start_x
self.tiles = tiles
self.entities = []
self.logs = Logs()
def add_entity(self, entity: "Entity") -> None:
"""
Register a new entity in the map.
"""
self.entities.append(entity)
entity.map = self
def remove_entity(self, entity: "Entity") -> None:
"""
Unregister an entity from the map.
"""
self.entities.remove(entity)
def find_entities(self, entity_class: type) -> list:
return [entity for entity in self.entities
if isinstance(entity, entity_class)]
def is_free(self, y: int, x: int) -> bool:
"""
Indicates that the case at the coordinates (y, x) is empty.
"""
return 0 <= y < self.height and 0 <= x < self.width and \
self.tiles[y][x].can_walk() and \
not any(entity.x == x and entity.y == y for entity in self.entities)
@staticmethod
def load(filename: str) -> "Map":
"""
Read a file that contains the content of a map, and build a Map object.
"""
with open(filename, "r") as f:
file = f.read()
return Map.load_from_string(file)
@staticmethod
def load_from_string(content: str) -> "Map":
"""
Load a map represented by its characters and build a Map object.
"""
lines = content.split("\n")
first_line = lines[0]
start_y, start_x = map(int, first_line.split(" "))
lines = [line for line in lines[1:] if line]
height = len(lines)
width = len(lines[0])
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return Map(width, height, tiles, start_y, start_x)
@staticmethod
def load_dungeon_from_string(content: str) -> List[List["Tile"]]:
"""
Transforms a string into the list of corresponding tiles
"""
lines = content.split("\n")
tiles = [[Tile.from_ascii_char(c)
for x, c in enumerate(line)] for y, line in enumerate(lines)]
return tiles
def draw_string(self, pack: TexturePack) -> str:
"""
Draw the current map as a string object that can be rendered
in the window.
"""
return "\n".join("".join(tile.char(pack) for tile in line)
for line in self.tiles)
def spawn_random_entities(self, count: int) -> None:
"""
Put randomly {count} hedgehogs on the map, where it is available.
"""
for ignored in range(count):
y, x = 0, 0
while True:
y, x = randint(0, self.height - 1), randint(0, self.width - 1)
tile = self.tiles[y][x]
if tile.can_walk():
break
entity = choice(Entity.get_all_entity_classes())()
entity.move(y, x)
self.add_entity(entity)
def tick(self) -> None:
"""
Trigger all entity events.
"""
for entity in self.entities:
entity.act(self)
def save_state(self) -> dict:
"""
Saves the map's attributes to a dictionary
"""
d = dict()
d["width"] = self.width
d["height"] = self.height
d["start_y"] = self.start_y
d["start_x"] = self.start_x
d["currentx"] = self.currentx
d["currenty"] = self.currenty
d["entities"] = []
for enti in self.entities:
d["entities"].append(enti.save_state())
d["map"] = self.draw_string(TexturePack.ASCII_PACK)
return d
def load_state(self, d: dict) -> None:
"""
Loads the map's attributes from a dictionary
"""
self.width = d["width"]
self.height = d["height"]
self.start_y = d["start_y"]
self.start_x = d["start_x"]
self.currentx = d["currentx"]
self.currenty = d["currenty"]
self.tiles = self.load_dungeon_from_string(d["map"])
self.entities = []
dictclasses = Entity.get_all_entity_classes_in_a_dict()
for entisave in d["entities"]:
self.add_entity(dictclasses[entisave["type"]](**entisave))
class Tile(Enum):
"""
The internal representation of the tiles of the map
"""
EMPTY = auto()
WALL = auto()
FLOOR = auto()
@staticmethod
def from_ascii_char(ch: str) -> "Tile":
"""
Maps an ascii character to its equivalent in the texture pack
"""
for tile in Tile:
if tile.char(TexturePack.ASCII_PACK) == ch:
return tile
raise ValueError(ch)
def char(self, pack: TexturePack) -> str:
"""
Translates a Tile to the corresponding character according
to the texture pack
"""
return getattr(pack, self.name)
def is_wall(self) -> bool:
"""
Is this Tile a wall?
"""
return self == Tile.WALL
def can_walk(self) -> bool:
"""
Check if an entity (player or not) can move in this tile.
"""
return not self.is_wall() and self != Tile.EMPTY
class Entity:
"""
An Entity object represents any entity present on the map
"""
y: int
x: int
name: str
map: Map
# noinspection PyShadowingBuiltins
def __init__(self, y: int = 0, x: int = 0, name: Optional[str] = None,
map: Optional[Map] = None, *ignored, **ignored2):
self.y = y
self.x = x
self.name = name
self.map = map
def check_move(self, y: int, x: int, move_if_possible: bool = False)\
-> bool:
"""
Checks if moving to (y,x) is authorized
"""
free = self.map.is_free(y, x)
if free and move_if_possible:
self.move(y, x)
return free
def move(self, y: int, x: int) -> bool:
"""
Moves an entity to (y,x) coordinates
"""
self.y = y
self.x = x
return True
def move_up(self, force: bool = False) -> bool:
"""
Moves the entity up one tile, if possible
"""
return self.move(self.y - 1, self.x) if force else \
self.check_move(self.y - 1, self.x, True)
def move_down(self, force: bool = False) -> bool:
"""
Moves the entity down one tile, if possible
"""
return self.move(self.y + 1, self.x) if force else \
self.check_move(self.y + 1, self.x, True)
def move_left(self, force: bool = False) -> bool:
"""
Moves the entity left one tile, if possible
"""
return self.move(self.y, self.x - 1) if force else \
self.check_move(self.y, self.x - 1, True)
def move_right(self, force: bool = False) -> bool:
"""
Moves the entity right one tile, if possible
"""
return self.move(self.y, self.x + 1) if force else \
self.check_move(self.y, self.x + 1, True)
def act(self, m: Map) -> None:
"""
Define the action of the entity that is ran each tick.
By default, does nothing.
"""
pass
def distance_squared(self, other: "Entity") -> int:
"""
Get the square of the distance to another entity.
Useful to check distances since square root takes time.
"""
return (self.y - other.y) ** 2 + (self.x - other.x) ** 2
def distance(self, other: "Entity") -> float:
"""
Get the cartesian distance to another entity.
"""
return sqrt(self.distance_squared(other))
def is_fighting_entity(self) -> bool:
"""
Is this entity a fighting entity?
"""
return isinstance(self, FightingEntity)
def is_item(self) -> bool:
"""
Is this entity an item?
"""
from squirrelbattle.entities.items import Item
return isinstance(self, Item)
@property
def translated_name(self) -> str:
return _(self.name.replace("_", " "))
@staticmethod
def get_all_entity_classes():
"""
Returns all entities subclasses
"""
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
from squirrelbattle.entities.monsters import Tiger, Hedgehog, \
Rabbit, TeddyBear
return [BodySnatchPotion, Bomb, Heart, Hedgehog,
Rabbit, TeddyBear, Tiger]
@staticmethod
def get_all_entity_classes_in_a_dict() -> dict:
"""
Returns all entities subclasses in a dictionary
"""
from squirrelbattle.entities.player import Player
from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \
TeddyBear
from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart
return {
"Tiger": Tiger,
"Bomb": Bomb,
"Heart": Heart,
"BodySnatchPotion": BodySnatchPotion,
"Hedgehog": Hedgehog,
"Rabbit": Rabbit,
"TeddyBear": TeddyBear,
"Player": Player,
}
def save_state(self) -> dict:
"""
Saves the coordinates of the entity
"""
d = dict()
d["x"] = self.x
d["y"] = self.y
d["type"] = self.__class__.__name__
return d
class FightingEntity(Entity):
"""
A FightingEntity is an entity that can fight, and thus has a health,
level and stats
"""
maxhealth: int
health: int
strength: int
intelligence: int
charisma: int
dexterity: int
constitution: int
level: int
def __init__(self, maxhealth: int = 0, health: Optional[int] = None,
strength: int = 0, intelligence: int = 0, charisma: int = 0,
dexterity: int = 0, constitution: int = 0, level: int = 0,
*args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.maxhealth = maxhealth
self.health = maxhealth if health is None else health
self.strength = strength
self.intelligence = intelligence
self.charisma = charisma
self.dexterity = dexterity
self.constitution = constitution
self.level = level
@property
def dead(self) -> bool:
return self.health <= 0
def hit(self, opponent: "FightingEntity") -> str:
"""
Deals damage to the opponent, based on the stats
"""
return _("{name} hits {opponent}.")\
.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:
"""
Take damage from the attacker, based on the stats
"""
self.health -= amount
if self.health <= 0:
self.die()
return _("{name} takes {amount} damage.")\
.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:
"""
If a fighting entity has no more health, it dies and is removed
"""
self.map.remove_entity(self)
def keys(self) -> list:
"""
Returns a fighting entities specific attributes
"""
return ["name", "maxhealth", "health", "level", "strength",
"intelligence", "charisma", "dexterity", "constitution"]
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
for name in self.keys():
d[name] = getattr(self, name)
return d

View File

@ -0,0 +1,201 @@
# German translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
#
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+0100\n"
"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/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== BESTAND =="
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Bestand:"
#: squirrelbattle/display/statsdisplay.py:50
msgid "YOU ARE DEAD"
msgstr "SIE WURDEN GESTORBEN"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
msgid "Bomb is exploding."
msgstr "Die Bombe explodiert."
#: squirrelbattle/entities/items.py:172
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} täuscht seinem Körper mit {entity} aus."
#: squirrelbattle/game.py:177
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:185
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:205
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."
#: squirrelbattle/interfaces.py:400
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} schlägt {opponent}."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} nimmt {amount} Schadenspunkte."
#: squirrelbattle/interfaces.py:414
#, python-brace-format
msgid "{name} dies."
msgstr "{name} stirbt."
#: squirrelbattle/menus.py:72
msgid "Back"
msgstr "Zurück"
#: squirrelbattle/tests/game_test.py:300 squirrelbattle/tests/game_test.py:303
#: squirrelbattle/tests/game_test.py:306
#: 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 "Key used to open the inventory"
msgstr "Bestandtaste"
#: squirrelbattle/tests/translations_test.py:47
msgid "Key used to use an item in the inventory"
msgstr "Taste um eines Objekts im Bestand zu verwenden"
#: squirrelbattle/tests/translations_test.py:49
msgid "Key used to equip an item in the inventory"
msgstr "Taste um eines Objekts im Bestand auszurüsten"
#: squirrelbattle/tests/translations_test.py:51
msgid "Key used to drop an item in the inventory"
msgstr "Taste um eines Objekts im Bestand zu werfen"
#: squirrelbattle/tests/translations_test.py:53
msgid "Texture pack"
msgstr "Textur-Packung"
#: squirrelbattle/tests/translations_test.py:54
msgid "Language"
msgstr "Sprache"
#: squirrelbattle/tests/translations_test.py:57
msgid "player"
msgstr "Spieler"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "Tiger"
#: squirrelbattle/tests/translations_test.py:60
msgid "hedgehog"
msgstr "Igel"
#: squirrelbattle/tests/translations_test.py:61
msgid "rabbit"
msgstr "Kanninchen"
#: squirrelbattle/tests/translations_test.py:62
msgid "teddy bear"
msgstr "Teddybär"
#: squirrelbattle/tests/translations_test.py:64
msgid "body snatch potion"
msgstr "Leichenfleddererzaubertrank"
#: squirrelbattle/tests/translations_test.py:65
msgid "bomb"
msgstr "Bombe"
#: squirrelbattle/tests/translations_test.py:66
msgid "heart"
msgstr "Herz"

View File

@ -0,0 +1,206 @@
# Spanish translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
# Translation by ifugaao
#
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ifugao\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
# Suggested in Weblate: == INVENTORIO ==
#: squirrelbattle/display/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== INVENTORIO =="
# Suggested in Weblate: Inventorio :
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Inventorio :"
# Suggested in Weblate: ERES MUERTO
#: squirrelbattle/display/statsdisplay.py:50
msgid "YOU ARE DEAD"
msgstr "ERES MUERTO"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
msgid "Bomb is exploding."
msgstr "La bomba está explotando."
#: squirrelbattle/entities/items.py:172
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} intercambió su cuerpo con {entity}."
#: squirrelbattle/game.py:177
msgid ""
"Some keys are missing in your save file.\n"
"Your save seems to be corrupt. It got deleted."
msgstr ""
"Algunas claves faltan en su archivo de guarda.\n"
"Su guarda parece a ser corruptido. Fue eliminado."
#: squirrelbattle/game.py:185
msgid ""
"No player was found on this map!\n"
"Maybe you died?"
msgstr ""
"No jugador encontrado sobre la carta !\n"
"¿ Quizas murió ?"
#: squirrelbattle/game.py:205
msgid ""
"The JSON file is not correct.\n"
"Your save seems corrupted. It got deleted."
msgstr ""
"El JSON archivo no es correcto.\n"
"Su guarda parece corrupta. Fue eliminada."
#: squirrelbattle/interfaces.py:400
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} golpea a {opponent}."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} recibe {amount} daño."
#: squirrelbattle/interfaces.py:414
#, python-brace-format
msgid "{name} dies."
msgstr "{name} se muere."
#: squirrelbattle/menus.py:72
msgid "Back"
msgstr "Volver"
#: squirrelbattle/tests/game_test.py:300,
#: squirrelbattle/tests/game_test.py:303,
#: squirrelbattle/tests/game_test.py:306,
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nuevo partido"
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Resumir"
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Cargar"
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Guardar"
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Parametros"
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Salir"
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Primera tecla para subir"
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Segunda tecla para subir"
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Primera tecla para bajar"
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Segunda tecla para bajar"
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Primera tecla para moverse a la izquierda"
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Segunda tecla para moverse a la izquierda"
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Primera tecla para moverse a la derecha"
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Segunda tecla para moverse a la derecha"
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Tecla para validar un menú"
#: squirrelbattle/tests/translations_test.py:45
msgid "Key used to open the inventory"
msgstr "Tecla para abrir el inventorio"
#: squirrelbattle/tests/translations_test.py:47
msgid "Key used to use an item in the inventory"
msgstr "Tecla para utilizar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:49
msgid "Key used to equip an item in the inventory"
msgstr "Tecla para equipar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:51
msgid "Key used to drop an item in the inventory"
msgstr "Tecla para dejar un objeto del inventorio"
#: squirrelbattle/tests/translations_test.py:53
msgid "Texture pack"
msgstr "Paquete de texturas"
#: squirrelbattle/tests/translations_test.py:54
msgid "Language"
msgstr "Languaje"
#: squirrelbattle/tests/translations_test.py:57
msgid "player"
msgstr "jugador"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:60
msgid "hedgehog"
msgstr "erizo"
#: squirrelbattle/tests/translations_test.py:61
msgid "rabbit"
msgstr "conejo"
#: squirrelbattle/tests/translations_test.py:62
msgid "teddy bear"
msgstr "osito de peluche"
#: squirrelbattle/tests/translations_test.py:64
msgid "body snatch potion"
msgstr "poción de intercambio"
#: squirrelbattle/tests/translations_test.py:65
msgid "bomb"
msgstr "bomba"
#: squirrelbattle/tests/translations_test.py:66
msgid "heart"
msgstr "corazón"

View File

@ -0,0 +1,202 @@
# French translation of Squirrel Battle
# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse
# This file is distributed under the same license as the squirrelbattle package.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: squirrelbattle 3.14.1\n"
"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n"
"POT-Creation-Date: 2020-12-05 14:46+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: squirrelbattle/display/menudisplay.py:105
msgid "== INVENTORY =="
msgstr "== INVENTAIRE =="
#: squirrelbattle/display/statsdisplay.py:34
msgid "Inventory:"
msgstr "Inventaire :"
#: squirrelbattle/display/statsdisplay.py:50
msgid "YOU ARE DEAD"
msgstr "VOUS ÊTES MORT"
#. The bomb is exploding.
#. Each entity that is close to the bomb takes damages.
#. The player earn XP if the entity was killed.
#: squirrelbattle/entities/items.py:128
msgid "Bomb is exploding."
msgstr "La bombe explose."
#: squirrelbattle/entities/items.py:172
#, python-brace-format
msgid "{player} exchanged its body with {entity}."
msgstr "{player} a échangé son corps avec {entity}."
#: squirrelbattle/game.py:177
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:185
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:205
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/interfaces.py:400
#, python-brace-format
msgid "{name} hits {opponent}."
msgstr "{name} frappe {opponent}."
#: squirrelbattle/interfaces.py:412
#, python-brace-format
msgid "{name} takes {amount} damage."
msgstr "{name} prend {amount} points de dégât."
#: squirrelbattle/interfaces.py:414
#, python-brace-format
msgid "{name} dies."
msgstr "{name} meurt."
#: squirrelbattle/menus.py:72
msgid "Back"
msgstr "Retour"
#: squirrelbattle/tests/game_test.py:300 squirrelbattle/tests/game_test.py:303
#: squirrelbattle/tests/game_test.py:306
#: squirrelbattle/tests/translations_test.py:16
msgid "New game"
msgstr "Nouvelle partie"
#: squirrelbattle/tests/translations_test.py:17
msgid "Resume"
msgstr "Continuer"
#: squirrelbattle/tests/translations_test.py:18
msgid "Load"
msgstr "Charger"
#: squirrelbattle/tests/translations_test.py:19
msgid "Save"
msgstr "Sauvegarder"
#: squirrelbattle/tests/translations_test.py:20
msgid "Settings"
msgstr "Paramètres"
#: squirrelbattle/tests/translations_test.py:21
msgid "Exit"
msgstr "Quitter"
#: squirrelbattle/tests/translations_test.py:27
msgid "Main key to move up"
msgstr "Touche principale pour aller vers le haut"
#: squirrelbattle/tests/translations_test.py:29
msgid "Secondary key to move up"
msgstr "Touche secondaire pour aller vers le haut"
#: squirrelbattle/tests/translations_test.py:31
msgid "Main key to move down"
msgstr "Touche principale pour aller vers le bas"
#: squirrelbattle/tests/translations_test.py:33
msgid "Secondary key to move down"
msgstr "Touche secondaire pour aller vers le bas"
#: squirrelbattle/tests/translations_test.py:35
msgid "Main key to move left"
msgstr "Touche principale pour aller vers la gauche"
#: squirrelbattle/tests/translations_test.py:37
msgid "Secondary key to move left"
msgstr "Touche secondaire pour aller vers la gauche"
#: squirrelbattle/tests/translations_test.py:39
msgid "Main key to move right"
msgstr "Touche principale pour aller vers la droite"
#: squirrelbattle/tests/translations_test.py:41
msgid "Secondary key to move right"
msgstr "Touche secondaire pour aller vers la droite"
#: squirrelbattle/tests/translations_test.py:43
msgid "Key to validate a menu"
msgstr "Touche pour valider un menu"
#: squirrelbattle/tests/translations_test.py:45
msgid "Key used to open the inventory"
msgstr "Touche utilisée pour ouvrir l'inventaire"
#: squirrelbattle/tests/translations_test.py:47
msgid "Key used to use an item in the inventory"
msgstr "Touche pour utiliser un objet de l'inventaire"
#: squirrelbattle/tests/translations_test.py:49
msgid "Key used to equip an item in the inventory"
msgstr "Touche pour équiper un objet de l'inventaire"
#: squirrelbattle/tests/translations_test.py:51
msgid "Key used to drop an item in the inventory"
msgstr "Touche pour jeter un objet de l'inventaire"
#: squirrelbattle/tests/translations_test.py:53
msgid "Texture pack"
msgstr "Pack de textures"
#: squirrelbattle/tests/translations_test.py:54
msgid "Language"
msgstr "Langue"
#: squirrelbattle/tests/translations_test.py:57
msgid "player"
msgstr "joueur"
#: squirrelbattle/tests/translations_test.py:59
msgid "tiger"
msgstr "tigre"
#: squirrelbattle/tests/translations_test.py:60
msgid "hedgehog"
msgstr "hérisson"
#: squirrelbattle/tests/translations_test.py:61
msgid "rabbit"
msgstr "lapin"
#: squirrelbattle/tests/translations_test.py:62
msgid "teddy bear"
msgstr "nounours"
#: squirrelbattle/tests/translations_test.py:64
msgid "body snatch potion"
msgstr "potion d'arrachage de corps"
#: squirrelbattle/tests/translations_test.py:65
msgid "bomb"
msgstr "bombe"
#: squirrelbattle/tests/translations_test.py:66
msgid "heart"
msgstr "cœur"

View File

@ -1,87 +1,80 @@
import sys # 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 .entities.player import Player
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:
"""
A Menu object is the logical representation of a menu in the game
"""
values: list values: list
def __init__(self): def __init__(self):
self.position = 0 self.position = 0
def go_up(self) -> None: def go_up(self) -> None:
"""
Moves the pointer of the menu on the previous value
"""
self.position = max(0, self.position - 1) self.position = max(0, self.position - 1)
def go_down(self) -> None: def go_down(self) -> None:
"""
Moves the pointer of the menu on the next value
"""
self.position = min(len(self.values) - 1, self.position + 1) self.position = min(len(self.values) - 1, self.position + 1)
def validate(self) -> Any: def validate(self) -> Any:
"""
Selects the value that is pointed by the menu pointer
"""
return self.values[self.position] return self.values[self.position]
class MainMenuValues(Enum): class MainMenuValues(Enum):
START = 'Nouvelle partie' """
RESUME = 'Continuer' Values of the main menu
SETTINGS = 'Paramètres' """
EXIT = 'Quitter' START = "New game"
RESUME = "Resume"
SAVE = "Save"
LOAD = "Load"
SETTINGS = "Settings"
EXIT = "Exit"
def __str__(self): def __str__(self):
return self.value return _(self.value)
class MainMenu(Menu): class MainMenu(Menu):
"""
A special instance of a menu : the main menu
"""
values = [e for e in MainMenuValues] values = [e for e in MainMenuValues]
def handle_key_pressed(self, key: KeyValues, game: Any) -> None:
"""
In the main menu, we can navigate through options.
"""
if key == KeyValues.DOWN:
self.go_down()
if key == KeyValues.UP:
self.go_up()
if key == KeyValues.ENTER:
option = self.validate()
if option == MainMenuValues.START:
game.new_game()
game.state = GameMode.PLAY
game.display_actions(DisplayActions.UPDATE)
elif option == MainMenuValues.RESUME:
game.state = GameMode.PLAY
elif option == MainMenuValues.SETTINGS:
game.state = GameMode.SETTINGS
elif option == MainMenuValues.EXIT:
sys.exit(0)
class SettingsMenu(Menu): class SettingsMenu(Menu):
"""
A special instance of a menu : the settings menu
"""
waiting_for_key: bool = False waiting_for_key: bool = False
def update_values(self, settings: Settings) -> None: def update_values(self, settings: Settings) -> None:
self.values = [] self.values = list(settings.__dict__.items())
for i, key in enumerate(settings.settings_keys): self.values.append(("RETURN", ["", _("Back")]))
s = settings.get_comment(key)
s += " : "
if self.waiting_for_key and i == self.position:
s += "?"
else:
s += getattr(settings, key).replace("\n", "\\n")
s += 8 * " " # Write over old text
self.values.append(s)
self.values.append("")
self.values.append("Changer le pack de textures n'aura effet")
self.values.append("qu'après avoir relancé le jeu.")
self.values.append("")
self.values.append("Retour (espace)")
def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str,
game: Any) -> None: game: Any) -> None:
""" """
Update settings In the setting menu, we van select a setting and change it
""" """
if not self.waiting_for_key: if not self.waiting_for_key:
# Navigate normally through the menu. # Navigate normally through the menu.
@ -95,20 +88,27 @@ class SettingsMenu(Menu):
self.go_down() self.go_down()
if key == KeyValues.UP: if key == KeyValues.UP:
self.go_up() self.go_up()
if key == KeyValues.ENTER and self.position < len(self.values) - 3: if key == KeyValues.ENTER and self.position < len(self.values) - 1:
# Change a setting # Change a setting
option = list(game.settings.settings_keys)[self.position] option = self.values[self.position][0]
if option == "TEXTURE_PACK": if option == "TEXTURE_PACK":
game.settings.TEXTURE_PACK = \ game.settings.TEXTURE_PACK = \
TexturePack.get_next_pack_name( TexturePack.get_next_pack_name(
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 'es' \
if game.settings.LOCALE == 'de' 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)
else: else:
option = list(game.settings.settings_keys)[self.position] option = self.values[self.position][0]
# Don't use an already mapped key # Don't use an already mapped key
if any(getattr(game.settings, opt) == raw_key if any(getattr(game.settings, opt) == raw_key
for opt in game.settings.settings_keys if opt != option): for opt in game.settings.settings_keys if opt != option):
@ -119,7 +119,12 @@ class SettingsMenu(Menu):
self.update_values(game.settings) self.update_values(game.settings)
class ArbitraryMenu(Menu): class InventoryMenu(Menu):
def __init__(self, values: list): player: Player
super().__init__()
self.values = values def update_player(self, player: Player) -> None:
self.player = player
@property
def values(self) -> list:
return self.player.inventory

View File

@ -0,0 +1,24 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
class ResourceManager:
"""
The ResourceManager loads resources at their right place,
and stores files in config directory.
"""
BASE_DIR = Path(__file__).resolve().parent / 'assets'
# FIXME This might not work on not-UNIX based systems.
CONFIG_DIR = Path.home() / '.config' / 'squirrel-battle'
@classmethod
def get_asset_path(cls, filename: str) -> str:
return str(cls.BASE_DIR / filename)
@classmethod
def get_config_path(cls, filename: str) -> str:
cls.CONFIG_DIR.mkdir(parents=True) if not cls.CONFIG_DIR.is_dir() \
else None
return str(cls.CONFIG_DIR / filename)

View File

@ -1,7 +1,14 @@
# 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 .translations import gettext as _
class Settings: class Settings:
""" """
@ -11,25 +18,21 @@ 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.KEY_INVENTORY = ['i', 'Key used to open the inventory']
self.KEY_LEFT_SECONDARY = \ self.KEY_USE = ['u', 'Key used to use an item in the inventory']
['KEY_LEFT', 'Touche secondaire pour aller vers la gauche'] self.KEY_EQUIP = ['e', 'Key used to equip an item in the inventory']
self.KEY_RIGHT_PRIMARY = \ self.KEY_DROP = ['r', 'Key used to drop an item in the inventory']
['d', 'Touche principale pour aller vers la droite'] self.TEXTURE_PACK = ['ascii', 'Texture pack']
self.KEY_RIGHT_SECONDARY = \ self.LOCALE = [locale.getlocale()[0][:2], 'Language']
['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)
@ -48,10 +51,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]:
@ -81,13 +84,14 @@ class Settings:
""" """
Loads the settings from a file Loads the settings from a file
""" """
if os.path.isfile("settings.json"): file_path = ResourceManager.get_config_path("settings.json")
with open("settings.json", "r") as f: if os.path.isfile(file_path):
with open(file_path, "r") as f:
self.loads_from_string(f.read()) self.loads_from_string(f.read())
def write_settings(self) -> None: def write_settings(self) -> None:
""" """
Dumps the settings into a file Dumps the settings into a file
""" """
with open("settings.json", "w") as f: with open(ResourceManager.get_config_path("settings.json"), "w") as f:
f.write(self.dumps_to_string()) f.write(self.dumps_to_string())

View File

@ -1,8 +1,15 @@
# 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
class TermManager: # pragma: no cover class TermManager: # pragma: no cover
"""
The TermManager object initializes the terminal, returns a screen object and
de-initializes the terminal after use
"""
def __init__(self): def __init__(self):
self.screen = curses.initscr() self.screen = curses.initscr()
# convert escapes sequences to curses abstraction # convert escapes sequences to curses abstraction

View File

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

View File

@ -1,9 +1,13 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest import unittest
from dungeonbattle.entities.items import Bomb, Heart, Item from squirrelbattle.entities.items import BodySnatchPotion, Bomb, Heart, Item
from dungeonbattle.entities.monsters import Hedgehog from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear
from dungeonbattle.entities.player import Player from squirrelbattle.entities.player import Player
from dungeonbattle.interfaces import Entity, Map from squirrelbattle.interfaces import Entity, Map
from squirrelbattle.resources import ResourceManager
class TestEntities(unittest.TestCase): class TestEntities(unittest.TestCase):
@ -11,7 +15,7 @@ class TestEntities(unittest.TestCase):
""" """
Load example map that can be used in tests. Load example map that can be used in tests.
""" """
self.map = Map.load("resources/example_map.txt") self.map = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.player = Player() self.player = Player()
self.map.add_entity(self.player) self.map.add_entity(self.player)
self.player.move(self.map.start_y, self.map.start_x) self.player.move(self.map.start_y, self.map.start_x)
@ -35,21 +39,20 @@ class TestEntities(unittest.TestCase):
""" """
Test some random stuff with fighting entities. Test some random stuff with fighting entities.
""" """
entity = Hedgehog() entity = Tiger()
self.map.add_entity(entity) self.map.add_entity(entity)
self.assertEqual(entity.maxhealth, 10) self.assertEqual(entity.maxhealth, 20)
self.assertEqual(entity.maxhealth, entity.health) self.assertEqual(entity.maxhealth, entity.health)
self.assertEqual(entity.strength, 3) self.assertEqual(entity.strength, 2)
self.assertIsNone(entity.hit(entity)) for _ in range(9):
self.assertFalse(entity.dead) self.assertEqual(entity.hit(entity),
self.assertIsNone(entity.hit(entity)) "Tiger hits tiger. Tiger takes 2 damage.")
self.assertFalse(entity.dead) self.assertFalse(entity.dead)
self.assertIsNone(entity.hit(entity)) self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
self.assertFalse(entity.dead) + "Tiger takes 2 damage. Tiger dies.")
self.assertIsNone(entity.hit(entity))
self.assertTrue(entity.dead) self.assertTrue(entity.dead)
entity = Hedgehog() entity = Rabbit()
self.map.add_entity(entity) self.map.add_entity(entity)
entity.move(15, 44) entity.move(15, 44)
# Move randomly # Move randomly
@ -61,13 +64,20 @@ class TestEntities(unittest.TestCase):
self.map.tick() self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
# Hedgehog should fight # Rabbit should fight
old_health = self.player.health old_health = self.player.health
self.map.tick() self.map.tick()
self.assertTrue(entity.y == 2 and entity.x == 6) self.assertTrue(entity.y == 2 and entity.x == 6)
self.assertEqual(old_health - entity.strength, self.player.health) self.assertEqual(old_health - entity.strength, self.player.health)
self.assertEqual(self.map.logs.messages[-1],
f"{entity.name.capitalize()} hits {self.player.name}. \
{self.player.name.capitalize()} takes {entity.strength} damage.")
# Fight the hedgehog # Fight the rabbit
old_health = entity.health
self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength)
self.assertFalse(entity.dead)
old_health = entity.health old_health = entity.health
self.player.move_down() self.player.move_down()
self.assertEqual(entity.health, old_health - self.player.strength) self.assertEqual(entity.health, old_health - self.player.strength)
@ -87,12 +97,13 @@ class TestEntities(unittest.TestCase):
self.assertFalse(item.held) self.assertFalse(item.held)
item.hold(self.player) item.hold(self.player)
self.assertTrue(item.held) self.assertTrue(item.held)
item.drop(2, 6) item.drop()
self.assertEqual(item.y, 2) self.assertEqual(item.y, 1)
self.assertEqual(item.x, 6) self.assertEqual(item.x, 6)
# Pick up item # Pick up item
self.player.move_down() self.player.move_left()
self.player.move_right()
self.assertTrue(item.held) self.assertTrue(item.held)
self.assertEqual(item.held_by, self.player) self.assertEqual(item.held_by, self.player)
self.assertIn(item, self.player.inventory) self.assertIn(item, self.player.inventory)
@ -104,17 +115,29 @@ class TestEntities(unittest.TestCase):
""" """
item = Bomb() item = Bomb()
hedgehog = Hedgehog() hedgehog = Hedgehog()
teddy_bear = TeddyBear()
self.map.add_entity(item) self.map.add_entity(item)
self.map.add_entity(hedgehog) self.map.add_entity(hedgehog)
self.map.add_entity(teddy_bear)
hedgehog.health = 2 hedgehog.health = 2
teddy_bear.health = 2
hedgehog.move(41, 42) hedgehog.move(41, 42)
teddy_bear.move(42, 41)
item.act(self.map) item.act(self.map)
self.assertFalse(hedgehog.dead) self.assertFalse(hedgehog.dead)
item.drop(42, 42) self.assertFalse(teddy_bear.dead)
self.player.move(42, 42)
item.hold(self.player)
item.use()
self.assertEqual(item.y, 42) self.assertEqual(item.y, 42)
self.assertEqual(item.x, 42) self.assertEqual(item.x, 42)
item.act(self.map) # Wait for the explosion
for ignored in range(5):
item.act(self.map)
self.assertTrue(hedgehog.dead) self.assertTrue(hedgehog.dead)
self.assertTrue(teddy_bear.dead)
bomb_state = item.save_state()
self.assertEqual(bomb_state["damage"], item.damage)
def test_hearts(self) -> None: def test_hearts(self) -> None:
""" """
@ -128,6 +151,26 @@ class TestEntities(unittest.TestCase):
self.assertNotIn(item, self.map.entities) self.assertNotIn(item, self.map.entities)
self.assertEqual(self.player.health, self.assertEqual(self.player.health,
self.player.maxhealth - item.healing) self.player.maxhealth - item.healing)
heart_state = item.save_state()
self.assertEqual(heart_state["healing"], item.healing)
def test_body_snatch_potion(self) -> None:
"""
Test some random stuff with body snatch potions.
"""
item = BodySnatchPotion()
self.map.add_entity(item)
item.hold(self.player)
tiger = Tiger(y=42, x=42)
self.map.add_entity(tiger)
# The player becomes a tiger, and the tiger becomes a squirrel
item.use()
self.assertEqual(self.player.name, "tiger")
self.assertEqual(tiger.name, "player")
self.assertEqual(self.player.y, 42)
self.assertEqual(self.player.x, 42)
def test_players(self) -> None: def test_players(self) -> None:
""" """
@ -144,7 +187,7 @@ class TestEntities(unittest.TestCase):
self.assertFalse(player.move_up()) self.assertFalse(player.move_up())
self.assertTrue(player.move_left()) self.assertTrue(player.move_left())
self.assertFalse(player.move_left()) self.assertFalse(player.move_left())
for i in range(8): for _ in range(8):
self.assertTrue(player.move_down()) self.assertTrue(player.move_down())
self.assertFalse(player.move_down()) self.assertFalse(player.move_down())
self.assertTrue(player.move_right()) self.assertTrue(player.move_right())
@ -158,3 +201,6 @@ class TestEntities(unittest.TestCase):
self.assertEqual(player.current_xp, 10) self.assertEqual(player.current_xp, 10)
self.assertEqual(player.max_xp, 40) self.assertEqual(player.max_xp, 40)
self.assertEqual(player.level, 4) self.assertEqual(player.level, 4)
player_state = player.save_state()
self.assertEqual(player_state["current_xp"], 10)

View File

@ -0,0 +1,419 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import unittest
from ..bootstrap import Bootstrap
from ..display.display import Display
from ..display.display_manager import DisplayManager
from ..entities.items import Bomb
from ..entities.player import Player
from ..enums import DisplayActions
from ..game import Game, KeyValues, GameMode
from ..menus import MainMenuValues
from ..resources import ResourceManager
from ..settings import Settings
from ..translations import gettext as _, Translator
class TestGame(unittest.TestCase):
def setUp(self) -> None:
"""
Setup game.
"""
self.game = Game()
self.game.new_game()
self.game.logs.add_message("Hello World !")
display = DisplayManager(None, self.game)
self.game.display_actions = display.handle_display_action
def test_load_game(self) -> None:
"""
Save a game and reload it.
"""
bomb = Bomb()
self.game.map.add_entity(bomb)
bomb.hold(self.game.player)
old_state = self.game.save_state()
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.ENTER) # Save game
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(), MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.ENTER) # Load game
new_state = self.game.save_state()
self.assertEqual(old_state, new_state)
# Ensure that the bomb is loaded
self.assertTrue(self.game.player.inventory)
# 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:
"""
Ensure that the test can't play the game,
because there is no associated shell.
Yeah, that's only for coverage.
"""
self.assertRaises(Exception, Bootstrap.run_game)
self.assertEqual(os.getenv("TERM", "unknown"), "unknown")
def test_key_translation(self) -> None:
"""
Test key bindings.
"""
self.game.settings = Settings()
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_PRIMARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_UP_SECONDARY, self.game.settings),
KeyValues.UP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_PRIMARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DOWN_SECONDARY, self.game.settings),
KeyValues.DOWN)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_PRIMARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_LEFT_SECONDARY, self.game.settings),
KeyValues.LEFT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_PRIMARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_RIGHT_SECONDARY, self.game.settings),
KeyValues.RIGHT)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_ENTER, self.game.settings),
KeyValues.ENTER)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_INVENTORY, self.game.settings),
KeyValues.INVENTORY)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_USE, self.game.settings),
KeyValues.USE)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_EQUIP, self.game.settings),
KeyValues.EQUIP)
self.assertEqual(KeyValues.translate_key(
self.game.settings.KEY_DROP, self.game.settings),
KeyValues.DROP)
self.assertEqual(KeyValues.translate_key(' ', self.game.settings),
KeyValues.SPACE)
self.assertEqual(KeyValues.translate_key('plop', self.game.settings),
None)
def test_key_press(self) -> None:
"""
Press a key and see what is done.
"""
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.RESUME)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.EXIT)
self.assertRaises(SystemExit, self.game.handle_key_pressed,
KeyValues.ENTER)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SETTINGS)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.LOAD)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.SAVE)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.RESUME)
self.game.handle_key_pressed(KeyValues.UP)
self.assertEqual(self.game.main_menu.validate(),
MainMenuValues.START)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.PLAY)
# Kill entities
for entity in self.game.map.entities.copy():
if not isinstance(entity, Player):
self.game.map.remove_entity(entity)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.DOWN)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y + 1)
self.assertEqual(new_x, x)
y, x = new_y, new_x
self.game.handle_key_pressed(KeyValues.RIGHT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x + 1)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.UP)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y - 1)
self.assertEqual(new_x, x)
y, x = self.game.player.y, self.game.player.x
self.game.handle_key_pressed(KeyValues.LEFT)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x - 1)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_new_game(self) -> None:
"""
Ensure that the start button starts a new game.
"""
old_map = self.game.map
old_player = self.game.player
self.game.handle_key_pressed(KeyValues.ENTER) # Start new game
new_map = self.game.map
new_player = self.game.player
# Ensure that
self.assertNotEqual(old_map, new_map)
self.assertNotEqual(old_player, new_player)
self.game.handle_key_pressed(KeyValues.SPACE)
old_map = new_map
old_player = new_player
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER) # Resume game
new_map = self.game.map
new_player = self.game.player
self.assertEqual(old_map, new_map)
self.assertEqual(old_player, new_player)
def test_settings_menu(self) -> None:
"""
Ensure that the settings menu is working properly.
"""
self.game.settings = Settings()
# Open settings menu
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.SETTINGS)
# Define the "move up" key to 'w'
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'w')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_UP_PRIMARY, 'w')
# Navigate to "move left"
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.UP)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
# Define the "move up" key to 'a'
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertTrue(self.game.settings_menu.waiting_for_key)
# Can't used a mapped key
self.game.handle_key_pressed(None, 's')
self.assertTrue(self.game.settings_menu.waiting_for_key)
self.game.handle_key_pressed(None, 'a')
self.assertFalse(self.game.settings_menu.waiting_for_key)
self.assertEqual(self.game.settings.KEY_LEFT_PRIMARY, 'a')
# Navigate to "texture pack"
for ignored in range(9):
self.game.handle_key_pressed(KeyValues.DOWN)
# Change texture pack
self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.TEXTURE_PACK, "squirrel")
self.game.handle_key_pressed(KeyValues.ENTER)
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, "es")
self.assertEqual(_("New game"), "Nuevo partido")
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.settings.LOCALE, "en")
self.assertEqual(_("New game"), "New game")
# Navigate to "back" button
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.ENTER)
self.assertEqual(self.game.state, GameMode.MAINMENU)
def test_logs(self) -> None:
"""
Tests the use of logs
"""
self.assertEqual(self.game.logs.messages, ["Hello World !"])
self.game.logs.add_messages(["Hello", "World"])
self.assertEqual(self.game.logs.messages, ["Hello World !",
"Hello", "World"])
self.game.logs.clear()
self.assertEqual(self.game.logs.messages, [])
def test_dead_screen(self) -> None:
"""
Kill player and render dead screen.
"""
self.game.state = GameMode.PLAY
# Kill player
self.game.player.take_damage(self.game.player,
self.game.player.health + 2)
y, x = self.game.player.y, self.game.player.x
for key in [KeyValues.UP, KeyValues.DOWN,
KeyValues.LEFT, KeyValues.RIGHT]:
self.game.handle_key_pressed(key)
new_y, new_x = self.game.player.y, self.game.player.x
self.assertEqual(new_y, y)
self.assertEqual(new_x, x)
def test_not_implemented(self) -> None:
"""
Check that some functions are not implemented, only for coverage.
"""
self.assertRaises(NotImplementedError, Display.display, None)
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)
def test_inventory_menu(self) -> None:
"""
Open the inventory menu and interact with items.
"""
self.game.state = GameMode.PLAY
# Open and close the inventory
self.game.handle_key_pressed(KeyValues.INVENTORY)
self.assertEqual(self.game.state, GameMode.INVENTORY)
self.game.handle_key_pressed(KeyValues.SPACE)
self.assertEqual(self.game.state, GameMode.PLAY)
# Add five bombs in the inventory
for ignored in range(5):
bomb = Bomb()
bomb.map = self.game.map
bomb.map.add_entity(bomb)
bomb.hold(self.game.player)
self.game.handle_key_pressed(KeyValues.INVENTORY)
self.assertEqual(self.game.state, GameMode.INVENTORY)
# Navigate in the menu
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.inventory_menu.position, 3)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.DOWN)
self.game.handle_key_pressed(KeyValues.UP)
self.game.handle_key_pressed(KeyValues.DOWN)
self.assertEqual(self.game.inventory_menu.position, 4)
# Equip key does nothing
self.game.handle_key_pressed(KeyValues.EQUIP)
# Drop an item
bomb = self.game.player.inventory[-1]
self.assertEqual(self.game.inventory_menu.validate(), bomb)
self.assertTrue(bomb.held)
self.assertEqual(bomb.held_by, self.game.player)
self.game.handle_key_pressed(KeyValues.DROP)
self.assertFalse(bomb.held)
self.assertIsNone(bomb.held_by)
self.assertIsNone(bomb.owner)
self.assertFalse(bomb.exploding)
self.assertEqual(bomb.y, self.game.player.y)
self.assertEqual(bomb.x, self.game.player.x)
# Use the bomb
bomb = self.game.player.inventory[-1]
self.assertEqual(self.game.inventory_menu.validate(), bomb)
self.assertTrue(bomb.held)
self.assertEqual(bomb.held_by, self.game.player)
self.game.handle_key_pressed(KeyValues.USE)
self.assertFalse(bomb.held)
self.assertIsNone(bomb.held_by)
self.assertEqual(bomb.owner, self.game.player)
self.assertTrue(bomb.exploding)
self.assertEqual(bomb.y, self.game.player.y)
self.assertEqual(bomb.x, self.game.player.x)

View File

@ -1,7 +1,11 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest import unittest
from dungeonbattle.display.texturepack import TexturePack from squirrelbattle.display.texturepack import TexturePack
from dungeonbattle.interfaces import Map, Tile from squirrelbattle.interfaces import Map, Tile
from squirrelbattle.resources import ResourceManager
class TestInterfaces(unittest.TestCase): class TestInterfaces(unittest.TestCase):
@ -18,7 +22,7 @@ class TestInterfaces(unittest.TestCase):
""" """
Try to load a map from a file. Try to load a map from a file.
""" """
m = Map.load("resources/example_map.txt") m = Map.load(ResourceManager.get_asset_path("example_map.txt"))
self.assertEqual(m.width, 52) self.assertEqual(m.width, 52)
self.assertEqual(m.height, 17) self.assertEqual(m.height, 17)

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,9 +1,16 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import unittest import unittest
from dungeonbattle.settings import Settings from squirrelbattle.settings import Settings
from squirrelbattle.translations import Translator
class TestSettings(unittest.TestCase): class TestSettings(unittest.TestCase):
def setUp(self) -> None:
Translator.setlocale("en")
def test_settings(self) -> None: def test_settings(self) -> None:
""" """
Ensure that settings are well loaded. Ensure that settings are well loaded.
@ -21,7 +28,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,66 @@
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(_("Key used to open the inventory"),
"Touche utilisée pour ouvrir l'inventaire")
self.assertEqual(_("Key used to use an item in the inventory"),
"Touche pour utiliser un objet de l'inventaire")
self.assertEqual(_("Key used to equip an item in the inventory"),
"Touche pour équiper un objet de l'inventaire")
self.assertEqual(_("Key used to drop an item in the inventory"),
"Touche pour jeter un objet de l'inventaire")
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(_("body snatch potion"), "potion d'arrachage de corps")
self.assertEqual(_("bomb"), "bombe")
self.assertEqual(_("heart"), "cœur")

View File

@ -0,0 +1,107 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
import gettext as gt
import os
import re
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", "es", "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:
if language == "en":
# Don't translate the main language
continue
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, ifugao",
"--msgid-bugs-address=squirrel-battle@crans.org",
"--sort-by-file",
"-o", file_name]
if file_name.is_file():
args.append("--join-existing")
with open(file_name, "r") as f:
content = f.read()
with open(file_name, "w") as f:
f.write(re.sub("#:.*\n", "", content))
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:
if language == "en":
continue
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()

View File

@ -6,13 +6,13 @@ envlist =
skipsdist = True skipsdist = True
[testenv] [testenv]
sitepackages = True sitepackages = False
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
pytest pytest
pytest-cov pytest-cov
commands = commands =
pytest --cov=dungeonbattle/ --cov-report=term-missing dungeonbattle/ pytest --cov=squirrelbattle/ --cov-report=term-missing squirrelbattle/
[testenv:linters] [testenv:linters]
deps = deps =
@ -23,7 +23,7 @@ deps =
pep8-naming pep8-naming
pyflakes pyflakes
commands = commands =
flake8 main.py dungeonbattle flake8 main.py squirrelbattle
[flake8] [flake8]
ignore = W503 ANN002 ANN003 ANN101 ANN102 ANN204 ANN205 ignore = W503 ANN002 ANN003 ANN101 ANN102 ANN204 ANN205