Merge branch 'master' into map_generation
# Conflicts: # dungeonbattle/game.py # squirrelbattle/mapgeneration/__init__.py # squirrelbattle/mapgeneration/randomwalk.py
This commit is contained in:
		
							
								
								
									
										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 @@
 | 
				
			|||||||
[](https://gitlab.crans.org/ynerant/dungeon-battle/-/commits/master)
 | 
					[](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master)
 | 
				
			||||||
[](https://gitlab.crans.org/ynerant/dungeon-battle/-/commits/master)
 | 
					[](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master)
 | 
				
			||||||
 | 
					[](https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest)
 | 
				
			||||||
 | 
					[](https://pypi.org/project/squirrel-battle/)
 | 
				
			||||||
 | 
					[](https://pypi.org/project/squirrel-battle/)
 | 
				
			||||||
 | 
					[](https://aur.archlinux.org/packages/python-squirrel-battle/)
 | 
				
			||||||
 | 
					[](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)
 | 
					 | 
				
			||||||
							
								
								
									
										26
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								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__":
 | 
				
			||||||
    Bootstrap.run_game()
 | 
					    parser = argparse.ArgumentParser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.add_argument("--makemessages", "-mm", action="store_true",
 | 
				
			||||||
 | 
					                        help="Extract translatable strings")
 | 
				
			||||||
 | 
					    parser.add_argument("--compilemessages", "-cm", action="store_true",
 | 
				
			||||||
 | 
					                        help="Compile translatable strings")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    args = parser.parse_args(sys.argv[1:])
 | 
				
			||||||
 | 
					    if args.makemessages:
 | 
				
			||||||
 | 
					        Translator.makemessages()
 | 
				
			||||||
 | 
					    elif args.compilemessages:
 | 
				
			||||||
 | 
					        Translator.compilemessages()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        Bootstrap.run_game()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										55
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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.assertFalse(entity.dead)
 | 
					            self.assertEqual(entity.hit(entity),
 | 
				
			||||||
        self.assertIsNone(entity.hit(entity))
 | 
					                             "Tiger hits tiger. Tiger takes 2 damage.")
 | 
				
			||||||
        self.assertFalse(entity.dead)
 | 
					            self.assertFalse(entity.dead)
 | 
				
			||||||
        self.assertIsNone(entity.hit(entity))
 | 
					        self.assertEqual(entity.hit(entity), "Tiger hits tiger. "
 | 
				
			||||||
        self.assertFalse(entity.dead)
 | 
					                         + "Tiger takes 2 damage. Tiger dies.")
 | 
				
			||||||
        self.assertIsNone(entity.hit(entity))
 | 
					 | 
				
			||||||
        self.assertTrue(entity.dead)
 | 
					        self.assertTrue(entity.dead)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        entity = Hedgehog()
 | 
					        entity = Rabbit()
 | 
				
			||||||
        self.map.add_entity(entity)
 | 
					        self.map.add_entity(entity)
 | 
				
			||||||
        entity.move(15, 44)
 | 
					        entity.move(15, 44)
 | 
				
			||||||
        # Move randomly
 | 
					        # Move randomly
 | 
				
			||||||
@@ -61,13 +64,20 @@ class TestEntities(unittest.TestCase):
 | 
				
			|||||||
        self.map.tick()
 | 
					        self.map.tick()
 | 
				
			||||||
        self.assertTrue(entity.y == 2 and entity.x == 6)
 | 
					        self.assertTrue(entity.y == 2 and entity.x == 6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Hedgehog should fight
 | 
					        # Rabbit should fight
 | 
				
			||||||
        old_health = self.player.health
 | 
					        old_health = self.player.health
 | 
				
			||||||
        self.map.tick()
 | 
					        self.map.tick()
 | 
				
			||||||
        self.assertTrue(entity.y == 2 and entity.x == 6)
 | 
					        self.assertTrue(entity.y == 2 and entity.x == 6)
 | 
				
			||||||
        self.assertEqual(old_health - entity.strength, self.player.health)
 | 
					        self.assertEqual(old_health - entity.strength, self.player.health)
 | 
				
			||||||
 | 
					        self.assertEqual(self.map.logs.messages[-1],
 | 
				
			||||||
 | 
					                         f"{entity.name.capitalize()} hits {self.player.name}. \
 | 
				
			||||||
 | 
					{self.player.name.capitalize()} takes {entity.strength} damage.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Fight the hedgehog
 | 
					        # Fight the rabbit
 | 
				
			||||||
 | 
					        old_health = entity.health
 | 
				
			||||||
 | 
					        self.player.move_down()
 | 
				
			||||||
 | 
					        self.assertEqual(entity.health, old_health - self.player.strength)
 | 
				
			||||||
 | 
					        self.assertFalse(entity.dead)
 | 
				
			||||||
        old_health = entity.health
 | 
					        old_health = entity.health
 | 
				
			||||||
        self.player.move_down()
 | 
					        self.player.move_down()
 | 
				
			||||||
        self.assertEqual(entity.health, old_health - self.player.strength)
 | 
					        self.assertEqual(entity.health, old_health - self.player.strength)
 | 
				
			||||||
@@ -87,12 +97,13 @@ class TestEntities(unittest.TestCase):
 | 
				
			|||||||
        self.assertFalse(item.held)
 | 
					        self.assertFalse(item.held)
 | 
				
			||||||
        item.hold(self.player)
 | 
					        item.hold(self.player)
 | 
				
			||||||
        self.assertTrue(item.held)
 | 
					        self.assertTrue(item.held)
 | 
				
			||||||
        item.drop(2, 6)
 | 
					        item.drop()
 | 
				
			||||||
        self.assertEqual(item.y, 2)
 | 
					        self.assertEqual(item.y, 1)
 | 
				
			||||||
        self.assertEqual(item.x, 6)
 | 
					        self.assertEqual(item.x, 6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Pick up item
 | 
					        # Pick up item
 | 
				
			||||||
        self.player.move_down()
 | 
					        self.player.move_left()
 | 
				
			||||||
 | 
					        self.player.move_right()
 | 
				
			||||||
        self.assertTrue(item.held)
 | 
					        self.assertTrue(item.held)
 | 
				
			||||||
        self.assertEqual(item.held_by, self.player)
 | 
					        self.assertEqual(item.held_by, self.player)
 | 
				
			||||||
        self.assertIn(item, self.player.inventory)
 | 
					        self.assertIn(item, self.player.inventory)
 | 
				
			||||||
@@ -104,17 +115,29 @@ class TestEntities(unittest.TestCase):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        item = Bomb()
 | 
					        item = Bomb()
 | 
				
			||||||
        hedgehog = Hedgehog()
 | 
					        hedgehog = Hedgehog()
 | 
				
			||||||
 | 
					        teddy_bear = TeddyBear()
 | 
				
			||||||
        self.map.add_entity(item)
 | 
					        self.map.add_entity(item)
 | 
				
			||||||
        self.map.add_entity(hedgehog)
 | 
					        self.map.add_entity(hedgehog)
 | 
				
			||||||
 | 
					        self.map.add_entity(teddy_bear)
 | 
				
			||||||
        hedgehog.health = 2
 | 
					        hedgehog.health = 2
 | 
				
			||||||
 | 
					        teddy_bear.health = 2
 | 
				
			||||||
        hedgehog.move(41, 42)
 | 
					        hedgehog.move(41, 42)
 | 
				
			||||||
 | 
					        teddy_bear.move(42, 41)
 | 
				
			||||||
        item.act(self.map)
 | 
					        item.act(self.map)
 | 
				
			||||||
        self.assertFalse(hedgehog.dead)
 | 
					        self.assertFalse(hedgehog.dead)
 | 
				
			||||||
        item.drop(42, 42)
 | 
					        self.assertFalse(teddy_bear.dead)
 | 
				
			||||||
 | 
					        self.player.move(42, 42)
 | 
				
			||||||
 | 
					        item.hold(self.player)
 | 
				
			||||||
 | 
					        item.use()
 | 
				
			||||||
        self.assertEqual(item.y, 42)
 | 
					        self.assertEqual(item.y, 42)
 | 
				
			||||||
        self.assertEqual(item.x, 42)
 | 
					        self.assertEqual(item.x, 42)
 | 
				
			||||||
        item.act(self.map)
 | 
					        # Wait for the explosion
 | 
				
			||||||
 | 
					        for ignored in range(5):
 | 
				
			||||||
 | 
					            item.act(self.map)
 | 
				
			||||||
        self.assertTrue(hedgehog.dead)
 | 
					        self.assertTrue(hedgehog.dead)
 | 
				
			||||||
 | 
					        self.assertTrue(teddy_bear.dead)
 | 
				
			||||||
 | 
					        bomb_state = item.save_state()
 | 
				
			||||||
 | 
					        self.assertEqual(bomb_state["damage"], item.damage)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_hearts(self) -> None:
 | 
					    def test_hearts(self) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -128,6 +151,26 @@ class TestEntities(unittest.TestCase):
 | 
				
			|||||||
        self.assertNotIn(item, self.map.entities)
 | 
					        self.assertNotIn(item, self.map.entities)
 | 
				
			||||||
        self.assertEqual(self.player.health,
 | 
					        self.assertEqual(self.player.health,
 | 
				
			||||||
                         self.player.maxhealth - item.healing)
 | 
					                         self.player.maxhealth - item.healing)
 | 
				
			||||||
 | 
					        heart_state = item.save_state()
 | 
				
			||||||
 | 
					        self.assertEqual(heart_state["healing"], item.healing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_body_snatch_potion(self) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test some random stuff with body snatch potions.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        item = BodySnatchPotion()
 | 
				
			||||||
 | 
					        self.map.add_entity(item)
 | 
				
			||||||
 | 
					        item.hold(self.player)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tiger = Tiger(y=42, x=42)
 | 
				
			||||||
 | 
					        self.map.add_entity(tiger)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The player becomes a tiger, and the tiger becomes a squirrel
 | 
				
			||||||
 | 
					        item.use()
 | 
				
			||||||
 | 
					        self.assertEqual(self.player.name, "tiger")
 | 
				
			||||||
 | 
					        self.assertEqual(tiger.name, "player")
 | 
				
			||||||
 | 
					        self.assertEqual(self.player.y, 42)
 | 
				
			||||||
 | 
					        self.assertEqual(self.player.x, 42)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_players(self) -> None:
 | 
					    def test_players(self) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -144,7 +187,7 @@ class TestEntities(unittest.TestCase):
 | 
				
			|||||||
        self.assertFalse(player.move_up())
 | 
					        self.assertFalse(player.move_up())
 | 
				
			||||||
        self.assertTrue(player.move_left())
 | 
					        self.assertTrue(player.move_left())
 | 
				
			||||||
        self.assertFalse(player.move_left())
 | 
					        self.assertFalse(player.move_left())
 | 
				
			||||||
        for i in range(8):
 | 
					        for _ in range(8):
 | 
				
			||||||
            self.assertTrue(player.move_down())
 | 
					            self.assertTrue(player.move_down())
 | 
				
			||||||
        self.assertFalse(player.move_down())
 | 
					        self.assertFalse(player.move_down())
 | 
				
			||||||
        self.assertTrue(player.move_right())
 | 
					        self.assertTrue(player.move_right())
 | 
				
			||||||
@@ -158,3 +201,6 @@ class TestEntities(unittest.TestCase):
 | 
				
			|||||||
        self.assertEqual(player.current_xp, 10)
 | 
					        self.assertEqual(player.current_xp, 10)
 | 
				
			||||||
        self.assertEqual(player.max_xp, 40)
 | 
					        self.assertEqual(player.max_xp, 40)
 | 
				
			||||||
        self.assertEqual(player.level, 4)
 | 
					        self.assertEqual(player.level, 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        player_state = player.save_state()
 | 
				
			||||||
 | 
					        self.assertEqual(player_state["current_xp"], 10)
 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user