Merge branch 'master' into map_generation
# Conflicts: # dungeonbattle/game.py # squirrelbattle/mapgeneration/__init__.py # squirrelbattle/mapgeneration/randomwalk.py
This commit is contained in:
commit
7db2dd2b46
15
.gitignore
vendored
15
.gitignore
vendored
@ -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
|
||||||
|
@ -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
674
COPYING
Normal 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>.
|
42
README.md
42
README.md
@ -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
118
debian/75-fix-squirrel-emojis.conf
vendored
Normal 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
5
debian/README.debian
vendored
Normal 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
11
debian/changelog
vendored
Normal 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
1
debian/compat
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
10
|
16
debian/control
vendored
Normal file
16
debian/control
vendored
Normal 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
29
debian/copyright
vendored
Normal 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
2
debian/install
vendored
Normal 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
5
debian/rules
vendored
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
export DH_VERBOSE = 1
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@ --with python3 --buildsystem=pybuild
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal 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
60
docs/conf.py
Normal 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
323
docs/deployment.rst
Normal 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
21
docs/display/index.rst
Normal 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
4
docs/display/logs.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Affichage de l'historique
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Pas encore documenté.
|
4
docs/display/map.rst
Normal file
4
docs/display/map.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Affichage de la carte
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Pas encore documenté.
|
4
docs/display/menu.rst
Normal file
4
docs/display/menu.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Affichage des menus
|
||||||
|
===================
|
||||||
|
|
||||||
|
Pas encore documenté.
|
4
docs/display/stats.rst
Normal file
4
docs/display/stats.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Affichage des statistiques
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Pas encore documenté.
|
31
docs/documentation.rst
Normal file
31
docs/documentation.rst
Normal 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
79
docs/entities/index.rst
Normal 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
50
docs/entities/items.rst
Normal 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 ``💜``.
|
55
docs/entities/monsters.rst
Normal file
55
docs/entities/monsters.rst
Normal 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
52
docs/entities/player.rst
Normal 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
54
docs/index.rst
Normal 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
40
docs/install-dev.rst
Normal 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
80
docs/install.rst
Normal 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
46
docs/map.rst
Normal 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
2
docs/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
sphinx
|
||||||
|
sphinx-rtd-theme
|
24
docs/rules.rst
Normal file
24
docs/rules.rst
Normal 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
4
docs/settings.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Paramètres
|
||||||
|
==========
|
||||||
|
|
||||||
|
Pas encore documenté.
|
12
docs/tests.rst
Normal file
12
docs/tests.rst
Normal 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
65
docs/texture-pack.rst
Normal 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
120
docs/translation.rst
Normal 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)``,
|
||||||
|
où ``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
47
docs/troubleshooting.rst
Normal 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.
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
24
main.py
24
main.py
@ -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__":
|
||||||
|
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()
|
Bootstrap.run_game()
|
||||||
|
55
setup.py
Normal file
55
setup.py
Normal 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",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
2
squirrelbattle/__init__.py
Normal file
2
squirrelbattle/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
24
squirrelbattle/bootstrap.py
Normal file
24
squirrelbattle/bootstrap.py
Normal 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)
|
2
squirrelbattle/display/__init__.py
Normal file
2
squirrelbattle/display/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
160
squirrelbattle/display/display.py
Normal file
160
squirrelbattle/display/display.py
Normal 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)
|
107
squirrelbattle/display/display_manager.py
Normal file
107
squirrelbattle/display/display_manager.py
Normal 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
|
25
squirrelbattle/display/logsdisplay.py
Normal file
25
squirrelbattle/display/logsdisplay.py
Normal 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)
|
71
squirrelbattle/display/mapdisplay.py
Normal file
71
squirrelbattle/display/mapdisplay.py
Normal 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)
|
120
squirrelbattle/display/menudisplay.py
Normal file
120
squirrelbattle/display/menudisplay.py
Normal 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
|
31
squirrelbattle/display/messagedisplay.py
Normal file
31
squirrelbattle/display/messagedisplay.py
Normal 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)
|
58
squirrelbattle/display/statsdisplay.py
Normal file
58
squirrelbattle/display/statsdisplay.py
Normal 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)
|
@ -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='🔀',
|
||||||
)
|
)
|
2
squirrelbattle/entities/__init__.py
Normal file
2
squirrelbattle/entities/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
178
squirrelbattle/entities/items.py
Normal file
178
squirrelbattle/entities/items.py
Normal 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)
|
102
squirrelbattle/entities/monsters.py
Normal file
102
squirrelbattle/entities/monsters.py
Normal 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)
|
153
squirrelbattle/entities/player.py
Normal file
153
squirrelbattle/entities/player.py
Normal 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
|
@ -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
216
squirrelbattle/game.py
Normal 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()))
|
438
squirrelbattle/interfaces.py
Normal file
438
squirrelbattle/interfaces.py
Normal 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
|
201
squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po
Normal file
201
squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po
Normal 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"
|
206
squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po
Normal file
206
squirrelbattle/locale/es/LC_MESSAGES/squirrelbattle.po
Normal 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"
|
202
squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po
Normal file
202
squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po
Normal 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"
|
@ -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
|
24
squirrelbattle/resources.py
Normal file
24
squirrelbattle/resources.py
Normal 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)
|
@ -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())
|
@ -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
|
2
squirrelbattle/tests/__init__.py
Normal file
2
squirrelbattle/tests/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
@ -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.assertEqual(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.assertFalse(entity.dead)
|
|
||||||
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)
|
||||||
|
# Wait for the explosion
|
||||||
|
for ignored in range(5):
|
||||||
item.act(self.map)
|
item.act(self.map)
|
||||||
self.assertTrue(hedgehog.dead)
|
self.assertTrue(hedgehog.dead)
|
||||||
|
self.assertTrue(teddy_bear.dead)
|
||||||
|
bomb_state = item.save_state()
|
||||||
|
self.assertEqual(bomb_state["damage"], item.damage)
|
||||||
|
|
||||||
def test_hearts(self) -> None:
|
def test_hearts(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -128,6 +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)
|
419
squirrelbattle/tests/game_test.py
Normal file
419
squirrelbattle/tests/game_test.py
Normal 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)
|
@ -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)
|
||||||
|
|
@ -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
|
@ -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')
|
66
squirrelbattle/tests/translations_test.py
Normal file
66
squirrelbattle/tests/translations_test.py
Normal 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")
|
107
squirrelbattle/translations.py
Normal file
107
squirrelbattle/translations.py
Normal 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()
|
6
tox.ini
6
tox.ini
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user