Compare commits

...

78 Commits

Author SHA1 Message Date
Nguyễn Gia Phong 12b798ac00 Employ linters 2020-09-19 11:12:54 +07:00
Nguyễn Gia Phong d97e1a1294 Update git repo and nitpick 2020-09-08 22:47:17 +07:00
Nguyễn Gia Phong c61bd8acc7 Update documentation pointers 2020-07-24 21:52:46 +07:00
Nguyễn Gia Phong 22b0e683bf Write an overvview in docs 2020-07-24 21:14:12 +07:00
Nguyễn Gia Phong 754610a095 Render math correctly and clean up docs 2020-07-24 20:54:56 +07:00
Nguyễn Gia Phong 328f6809f8 Specify Sphinx version 2020-07-20 21:54:53 +07:00
Nguyễn Gia Phong 1b7bf9833d Use Sphinx to replace wiki 2020-07-20 21:46:31 +07:00
Nguyễn Gia Phong d3839f160e Nitpick around 2020-04-28 17:50:36 +07:00
Nguyễn Gia Phong bc8579329e Use palace.MessageHandler to collect stopped sources 2020-04-25 15:19:05 +07:00
Nguyễn Gia Phong 2813a0856f Switch audio plane to Oxy
Also clean up sources properly
2020-04-12 17:51:12 +07:00
Nguyễn Gia Phong 600c72d0d4 Use palace for positional audio rendering
This fixes GH-15.  Sources doesn't seem to be cleaned up properly though.
2020-04-12 16:35:44 +07:00
Nguyễn Gia Phong c326f93bbb Redirect pygame message and add wiki to sdist 2020-03-04 09:41:05 +07:00
Nguyễn Gia Phong b90e6c272c Switch to flit for build 2020-03-03 21:48:00 +07:00
Nguyễn Gia Phong 5e5778d814 Drop Python 2 support (resolve #13) 2020-01-21 15:18:19 +07:00
Nguyễn Gia Phong e2149b18c2 Remove debugging print and improve style 2019-10-13 17:45:44 +07:00
Nguyễn Gia Phong 7d346a219a Prevent the maze from trapping the hero 2019-10-12 21:47:22 +07:00
Nguyễn Gia Phong b70c00eb8d Unnest the queue used for path finding 2019-10-09 17:20:57 +07:00
Nguyễn Gia Phong bb3d4158ca Remove useless enemy weights and clean up 2019-10-09 12:43:22 +07:00
Nguyễn Gia Phong ef70806a48 Fix typos and optimizations 2019-10-09 11:15:55 +07:00
Nguyễn Gia Phong 622df8c361 Fix zombie enemies and heart rate 2019-07-24 13:20:31 +07:00
Nguyễn Gia Phong 4f18daa234 Make icon true squircle 2019-07-16 16:13:37 +07:00
Nguyễn Gia Phong e2562e1698 Prevent player from creating enemy when there isn't any 2019-03-18 12:34:18 +07:00
Nguyễn Gia Phong 7a0ace220c Switch back to thrilling white noise 2019-03-15 20:23:43 +07:00
Nguyễn Gia Phong be6c2fedea Update documentation related to #11 2018-10-28 15:50:13 +07:00
Nguyễn Gia Phong bad902d02e Improve coding style 2018-10-09 21:20:38 +07:00
Nguyễn Gia Phong eb088c0cf1 Update documentation on touch control 2018-10-08 22:07:30 +07:00
Nguyễn Gia Phong 377dda3db0 Implement touch-friendly control 2018-10-07 21:59:39 +07:00
Nguyễn Gia Phong 865a3e3b71 Clarify what happens behind mouse move and close #10 2018-08-28 23:05:44 +07:00
Nguyễn Gia Phong 1e7e981e81 Fix stupid ass bugs 2018-08-08 20:05:21 +07:00
Nguyễn Gia Phong 6d2f6d6ad3 Add further description on game recording 2018-08-06 21:59:00 +07:00
Nguyễn Gia Phong d6dfd43158 Add HTML5 record player and update hit-and-run client 2018-08-05 18:24:05 +07:00
Nguyễn Gia Phong ba2aaeb1f1 Update socket output 2018-08-05 18:05:07 +07:00
Nguyễn Gia Phong 834fa33ec0 Implement game recording
And fix various bugs on game data export. Somehow they remain
undiscovered the last 5 months.
2018-08-03 22:50:59 +07:00
Nguyễn Gia Phong eb23230acb Use WASD instead of arrows for a less awkward default keybindings 2018-07-25 11:26:38 +07:00
Nguyễn Gia Phong 6dc590834e Revise documentation and bump to version 0.8 2018-07-20 15:01:03 +07:00
Nguyễn Gia Phong ffe6ba9855 Chocolate gets you high 2018-07-02 11:04:46 +07:00
Nguyễn Gia Phong fc05e0ccee Prevent gang-bang and re-balance gameplay 2018-06-28 10:31:04 +07:00
Nguyễn Gia Phong 3507a52ac7 Fix enemy spawning sound position 2018-06-28 10:31:04 +07:00
Nguyễn Gia Phong e7d04930b3 Use a more neutral algorithm to generate maze (#6) 2018-06-28 10:31:04 +07:00
Nguyễn Gia Phong 654a1a2c5e Fix bug that walls out of display can still be turned into enemies 2018-05-31 22:07:07 +07:00
Nguyễn Gia Phong cbaec90dd1 Add option to switch to the original music 2018-05-22 21:15:01 +07:00
Nguyễn Gia Phong 8e6faa6d26 Fix Python 3 incompatibility 2018-05-22 20:44:22 +07:00
Nguyễn Gia Phong eace9a270b Use more relaxing background music 2018-05-22 09:54:10 +07:00
Nguyễn Gia Phong 9a896890fa Turn wall grids into enemies when they are shot 2018-05-22 09:54:08 +07:00
Nguyễn Gia Phong 9dff378b57 Allow moving hero using mouse 2018-05-20 20:48:51 +07:00
Nguyễn Gia Phong 92a41b3cff Fix broken argument parser on Windows 2018-04-04 23:35:43 +07:00
Le Minh Nghia e63a1d8dc8 Add C# client example (#9) 2018-03-26 12:20:47 +07:00
Nguyễn Gia Phong 32e29b0a38 Fix grammar and typos 2018-03-22 23:01:00 +07:00
Nguyễn Gia Phong bbc98be317 Add socket client example and fix enemy-fall-of-the-map bug 2018-03-20 14:17:17 +07:00
Nguyễn Gia Phong 2bd7352aec Remove crazy score for server testing 2018-03-19 15:30:30 +07:00
Nguyễn Gia Phong ace9586778 Fix several bugs
* Set connection timeout to avoid hanging along with the client
* Now visible Chameleons are exported in server mode
* Disable manual slashing's bullets blocking so that there won't be no delay after this type of attack that make aiming stiff
2018-03-10 18:19:12 +07:00
Nguyễn Gia Phong d7eb9071a0 Shrink socket hardcoded msg lengths to 7 2018-03-07 17:04:48 +07:00
Nguyễn Gia Phong 5fa4eac9a8 Update documentation 2018-03-07 16:41:28 +07:00
Nguyễn Gia Phong 97d4a43ec7 Drop (trivial) OpenGL support 2018-03-07 16:13:34 +07:00
Nguyễn Gia Phong b5039285d5 Retain game state after pauses 2018-03-06 21:01:27 +07:00
Nguyễn Gia Phong f7c600934e Test and fix minor bugs 2018-03-06 09:58:52 +07:00
Nguyễn Gia Phong 2bafc0c75a Add documentation for remote control 2018-03-05 23:59:02 +07:00
Nguyễn Gia Phong 3cf78b680a Add time stamps to server log and anti-cheat on bullets blocking 2018-03-02 23:57:08 +07:00
Nguyễn Gia Phong 7bd13996fb Add command-line options for socket server 2018-03-02 22:28:49 +07:00
Nguyễn Gia Phong 6f9eb44e2a Enable manual slashing by moving mouse 2018-03-01 20:58:41 +07:00
Nguyễn Gia Phong 79ae3ed383 Specify imports, fix frozen bullets on game-over and uniform object value export 2018-02-28 22:06:03 +07:00
Nguyễn Gia Phong 781b347fcb Make remote control sticky and revise headless server 2018-02-27 22:25:58 +07:00
Nguyễn Gia Phong 0cfeaf9cab Make Agent Orange more lethal and noticable 2018-02-27 20:55:23 +07:00
Nguyễn Gia Phong 0ba2f33bda Relicense to AGPLv3+ and update copyright notices 2018-02-27 20:43:25 +07:00
Nguyễn Gia Phong b4c8f32700 Rework on socket server to make it really works (#7) 2018-02-27 16:34:00 +07:00
Nguyễn Gia Phong 3e85f0c3a1 Finnish first socket server protype 2018-02-26 21:02:11 +07:00
Nguyễn Gia Phong bc47fb3f30 Add sound options and semi-separate front-end from engine 2018-02-19 16:55:55 +07:00
Nguyễn Gia Phong 8852a9f678 Use pygame.Clock.get_fps to replace manual calculation 2018-02-19 01:01:14 +07:00
Nguyễn Gia Phong 77bb9a53d6 Partially refactor to prepare for alternate control from socket 2018-02-16 20:25:32 +07:00
Nguyễn Gia Phong e1db0393f9 Add CLI option to copy default config and link doc to wiki 2018-02-14 22:31:57 +07:00
Nguyễn Gia Phong 8095bcab8d Write documentation for config 2018-02-13 22:17:04 +07:00
Nguyễn Gia Phong 6a4f7e47ae Add argument parser 2018-02-13 20:51:41 +07:00
Nguyễn Gia Phong 465be4493d Clean up 2018-02-12 14:23:23 +07:00
Nguyễn Gia Phong 50a839826d Add control configuration 2018-02-08 20:41:08 +07:00
Nguyễn Gia Phong dbe0ae4c01 Add user configuration for graphics 2018-02-02 23:33:36 +07:00
Nguyễn Gia Phong 1b2dc5e169 Make all sound playing fail-safe 2018-01-28 11:48:41 +07:00
Nguyễn Gia Phong fca23ea3ce Lower ridiculously high maximum FPS 2018-01-28 11:43:06 +07:00
Nguyễn Gia Phong f9e4ab3ef1 Fix spawn sound volume 2018-01-28 09:06:28 +07:00
66 changed files with 2690 additions and 685 deletions

110
.gitignore vendored
View File

@ -1,6 +1,104 @@
brutalmaze.egg-info
build
dist
wiki
__pycache__
*.pyc
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/

147
LICENSE
View File

@ -1,23 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://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 GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
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
our General Public Licenses are 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.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@ -26,44 +24,34 @@ 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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
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.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
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.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
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.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@ -549,35 +537,45 @@ 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.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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
under version 3 of the GNU 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.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
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
the GNU Affero 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
Program specifies that a certain numbered version of the GNU Affero 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
GNU Affero 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
versions of the GNU Affero 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.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
it under the terms of the GNU Affero 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.
GNU Affero 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/>.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://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:
<program> Copyright (C) <year> <name of author>
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".
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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>.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@ -1 +0,0 @@
include README.rst LICENSE screenshot.png

View File

@ -1,57 +1,131 @@
Brutal Maze
===========
Brutal Maze is a hash and slash game with fast-paced action and a minimalist
art style.
Brutal Maze is a thrilling shoot 'em up game with minimalist art style.
.. image:: https://raw.githubusercontent.com/McSinyx/brutalmaze/master/screenshot.png
.. image:: https://brutalmaze.rtfd.io/_images/screenshot.png
:target: https://brutalmaze.rtfd.io/recplayer.html
The game features a trigon trapped in an infinite maze. As our hero tries to
escape, the maze's border turns into aggressive squares trying to stop him.
Your job is to help the trigon fight against those evil squares and find a way
out (if there is any). Be aware that the more get killed, the more will show up
and our hero will get weaker when wounded.
The game features a trigon trapped in an infinite maze. As our hero tries
to escape, the maze's border turns into aggressive squares trying to stop per.
Your job is to help the trigon fight against those evil squares and find
a way out (if there is any). Be aware that the more get killed,
the more will show up and our hero will get weaker when wounded.
Brutal Maze has a few notable feautures:
Brutal Maze has a few notable features:
* Being highly portable.
* Auto-generated and infinite maze.
* Auto-generated and infinite maze. [0]_
* No binary data for drawing.
* Enemies with special abilities: stun, poison, camo, etc.
* Somewhat a realistic physic and logic system.
* Resizable game window in-game.
* Easily customizable via INI file format.
* Recordable in JSON (some kind of silent screencast).
* Remote control through TCP/IP socket (can be used in AI researching).
Installation
------------
Brutal Maze is written in Python and is compatible with both version 2 and 3.
The installation procedure should be as simply as follow:
Brutal Maze is written in Python and is compatible version 3.6 and above.
The installation procedure should be as simple as follows:
* Install Python and `pip <https://pip.pypa.io/en/latest/>`_. Make sure the
directory for `Python scripts <https://docs.python.org/2/install/index.html#alternate-installation-the-user-scheme>`_
* Install Python and pip_. Make sure the directory for `Python scripts`_
is in your ``$PATH``.
* Open Terminal or Command Prompt and run ``pip install --user brutalmaze``.
Now you can lauch the game by running the command ``brutalmaze``.
For more information, see the `Installation <https://github.com/McSinyx/brutalmaze/wiki/Installation>`_
from Brutal Maze wiki.
For more information, see Installation_ page from the documentation.
Control
-------
After installation, you can launch the game by running the command
``brutalmaze``. Below are the default bindings, which can be configured as
shown in the next section:
F2
New game.
Escape, ``p``
Pause.
Up, ``w``
Move up.
Down, ``s``
Move down.
Left, ``a``
``p``
Toggle pause.
``m``
Toggle mute.
``a``
Move left.
Right, ``d``
``d``
Move right.
``w``
Move up.
``s``
Move down.
Left Mouse
Long-range attack.
Return, Right Mouse
Right Mouse
Close-range attack, also dodge from bullets.
Additionally, Brutal Maze also supports touch-friendly control. In this mode,
touches on different grid (empty, wall, enemy, hero) send different signals
(to guide the hero to either move or attack, or start new game). Albeit it is
implemented using *mouse button up* event, touch control is not a solution for
mouse-only input, but an attempt to support mobile GNU/Linux distribution such
as postmarketOS, i.e. it's meant to be played using two thumbs :-)
Configuration
-------------
Brutal Maze supports both configuration file and command-line options.
Apparently, while settings for graphics, sound and socket server can be set
either in the config file or using CLI, keyboard and mouse bindings are limited
to configuration file only.
Settings are read in the following order:
0. Default configuration [1]_
1. System-wide configuration file [2]_
2. Local configuration file [2]_
3. Manually set configuration file [3]_
4. Command-line arguments
Later-read preferences will override previous ones.
Remote control
--------------
If you enable the socket server [4]_, Brutal Maze will no longer accept
direct input from your mouse or keyboard, but wait for a client to connect.
The I/O format is explained in details in the `Remote Control`_ page.
Game recording
--------------
Either game played by human or client script can be recorded to JSON format.
This can be enabled by setting the output directory to a non-empty string [5]_.
Recordings can be played using Brutal Maze `HTML5 record player`_.
Copying
-------
Brutal Maze's source code and its icon are released under GNU Affero General
Public License version 3 or later. This means if you run a modified program on
a server and let other users communicate with it there, your server must also
allow them to download the source code corresponding to the modified version
running there.
This project also uses Tango color palette and several sound effects, whose
authors and licenses are listed in the Copying_ page in our documentation.
.. [0] Broken on vanilla pygame on GNU/Linux. For workarounds, see issue
`#3 <https://git.disroot.org/McSinyx/brutalmaze/issues/3>`_.
.. [1] This can be copied to desired location by ``brutalmaze --write-config
PATH``. ``brutalmaze --write-config`` alone will print the file to stdout.
.. [2] These will be listed as fallback config in the help message
(``brutalmaze --help``). See the Configuration_ documentation for more info.
.. [3] If specified by ``brutalmaze --config PATH``.
.. [4] This can be done by either editing option *Enable* in section *Server*
in the configuration file or launching the game via ``brutalmaze --server``.
.. [5] ``brutalmaze --record-dir DIR``. Navigate to Configuration_
to see more options.
.. _pip: https://pip.pypa.io/en/latest/
.. _Python scripts: https://docs.python.org/3/install/index.html#alternate-installation-the-user-scheme
.. _Installation: https://brutalmaze.rtfd.io/install.html
.. _Remote Control: https://brutalmaze.rtfd.io/remote.html
.. _HTML5 record player: https://brutalmaze.rtfd.io/recplayer.html
.. _Copying: https://brutalmaze.rtfd.io/copying.html
.. _Configuration: https://brutalmaze.rtfd.io/config.html

View File

@ -1,5 +1,3 @@
"""Brutal Maze is a hash and slash game with fast-paced action and a
minimalist art style.
"""
"""Minimalist thrilling shoot 'em up game with minimalist art style"""
from .main import main
from .game import __version__

View File

@ -1,35 +1,33 @@
# -*- coding: utf-8 -*-
# characters.py - module for hero and enemy classes
# This file is part of brutalmaze
# Copyright (C) 2017-2020 Nguyễn Gia Phong
#
# brutalmaze 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 file is part of Brutal Maze.
#
# brutalmaze is distributed in the hope that it will be useful,
# Brutal Maze is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Brutal Maze 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.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with brutalmaze. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__doc__ = 'brutalmaze module for hero and enemy classes'
__doc__ = 'Brutal Maze module for hero and enemy classes'
from collections import deque
from math import atan, atan2, sin, pi
from math import atan2, gcd, pi, sin
from random import choice, randrange, shuffle
from sys import modules
import pygame
from pygame.mixer import Sound
from pygame.time import get_ticks
from .constants import *
from .misc import sign, cosin, randsign, regpoly, fill_aapolygon, choices, play
from .constants import (ADJACENTS, AROUND_HERO, ATTACK_SPEED, EMPTY,
ENEMIES, ENEMY, ENEMY_HP, ENEMY_SPEED, FIRANGE,
HEAL_SPEED, HERO_HP, MIDDLE, MIN_BEAT, SFX_HEART,
SFX_SLASH_HERO, SFX_SPAWN, SQRT2, TANGO, WALL)
from .misc import fill_aapolygon, play, randsign, regpoly, sign
from .weapons import Bullet
@ -42,63 +40,111 @@ class Hero:
angle (float): angle of the direction the hero pointing (in radians)
color (tuple of pygame.Color): colors of the hero on different HPs
R (int): circumradius of the regular triangle representing the hero
next_heal (int): the tick that the hero gains back healing ability
next_beat (int): the tick to play next heart beat
next_strike (int): the tick that the hero can do the next attack
slashing (bool): flag indicates if the hero is doing close-range attack
firing (bool): flag indicates if the hero is doing long-range attack
dead (bool): flag indicates if the hero is dead
next_heal (float): minimum wound in ATTACK_SPEED allowing healing again
next_beat (float): time until next heart beat (in ms)
next_strike (float): time until the hero can do the next attack (in ms)
highness (float): likelihood that the hero shoots toward other angles
slashing (bool): flag indicating if the hero's doing close-range attack
firing (bool): flag indicating if the hero is doing long-range attack
dead (bool): flag indicating if the hero is dead
spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning
wound (float): amount of wound
sfx_heart (Sound): heart beat sound effect
wounds (deque of float): wounds in time of an attack (ATTACK_SPEED)
"""
def __init__(self, surface, fps):
def __init__(self, surface, fps, maze_size):
self.surface = surface
w, h = self.surface.get_width(), self.surface.get_height()
w, h = maze_size
self.x, self.y = w >> 1, h >> 1
self.angle, self.color = pi / 4, TANGO['Aluminium']
self.angle, self.color = -pi * 3 / 4, TANGO['Aluminium']
self.R = (w * h / sin(pi*2/3) / 624) ** 0.5
self.next_heal = self.next_beat = self.next_strike = 0
self.next_heal = -1.0
self.next_beat = self.next_strike = 0.0
self.highness = 0.0
self.slashing = self.firing = self.dead = False
self.spin_speed = fps / HERO_HP
self.spin_queue = self.wound = 0.0
self.sfx_heart = Sound(SFX_HEART)
self.wounds = deque([0.0])
def update(self, fps):
"""Update the hero."""
if self.dead:
self.spin_queue = 0.0
return
old_speed, time = self.spin_speed, get_ticks()
old_speed = self.spin_speed
self.spin_speed = fps / (HERO_HP-self.wound**0.5)
self.spin_queue *= self.spin_speed / old_speed
if time > self.next_heal:
if len(self.wounds) > fps * ATTACK_SPEED / 1000: self.wounds.popleft()
if sum(self.wounds) < self.next_heal: self.next_heal = -1.0
self.wound += self.wounds[-1]
if self.next_heal < 0:
self.wound -= HEAL_SPEED / self.spin_speed / HERO_HP
if self.wound < 0: self.wound = 0.0
if time > self.next_beat:
self.sfx_heart.play()
self.next_beat = time + MIN_BEAT*(2 - self.wound/HERO_HP)
self.wounds.append(0.0)
if self.next_beat <= 0:
play(SFX_HEART)
self.next_beat = MIN_BEAT*(2 - self.wound/HERO_HP)
else:
self.next_beat -= 1000 / fps
self.next_strike -= 1000 / fps
if self.slashing and time >= self.next_strike:
self.next_strike = time + ATTACK_SPEED
full_spin = pi * 2 / self.sides
if self.slashing and self.next_strike <= 0:
self.next_strike = ATTACK_SPEED
self.spin_queue = randsign() * self.spin_speed
if abs(self.spin_queue) > 0.5:
self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
self.angle -= sign(self.spin_queue) * full_spin
if round(self.spin_queue) != 0:
self.angle += sign(self.spin_queue) * full_spin / self.spin_speed
self.spin_queue -= sign(self.spin_queue)
else:
# Follow the mouse cursor
x, y = pygame.mouse.get_pos()
self.angle = atan2(y - self.y, x - self.x)
self.spin_queue = 0.0
trigon = regpoly(3, self.R, self.angle, self.x, self.y)
fill_aapolygon(self.surface, trigon, self.color[int(self.wound)])
def resize(self):
@property
def sides(self):
"""Number of sides the hero has. While the hero is generally
a trigon, Agent Orange may turn him into a square.
"""
return 3 if self.next_heal < 0 else 4
def update_angle(self, angle):
"""Turn to the given angle if the hero is not busy slashing."""
if round(self.spin_queue) != 0: return
delta = (angle - self.angle + pi) % (pi * 2) - pi
unit = pi * 2 / self.sides / self.spin_speed
if abs(delta) < unit:
self.angle, self.spin_queue = angle, 0.0
else:
self.spin_queue = delta / unit
@property
def shots(self):
"""List of Bullet the hero has just shot."""
if not self.firing or self.slashing or self.next_strike > 0: return []
self.next_strike = ATTACK_SPEED
if not randrange(int(self.highness + 1)):
return [Bullet(self.surface, self.x, self.y,
self.angle, 'Aluminium')]
self.highness -= 1.0
n = self.sides
corners = {randrange(n) for _ in range(n)}
angles = (self.angle + pi*2*corner/n for corner in corners)
return [Bullet(self.surface, self.x, self.y, angle, 'Aluminium')
for angle in angles]
def get_color(self):
"""Return current color of the hero."""
return self.color[int(self.wound)]
def draw(self):
"""Draw the hero."""
trigon = regpoly(self.sides, self.R, self.angle, self.x, self.y)
fill_aapolygon(self.surface, trigon, self.get_color())
def resize(self, maze_size):
"""Resize the hero."""
w, h = self.surface.get_width(), self.surface.get_height()
w, h = maze_size
self.x, self.y = w >> 1, h >> 1
self.R = (w * h / sin(pi*2/3) / 624) ** 0.5
@ -111,41 +157,51 @@ class Enemy:
x, y (int): coordinates of the center of the enemy (in grids)
angle (float): angle of the direction the enemy pointing (in radians)
color (str): enemy's color name
awake (bool): flag indicates if the enemy is active
next_strike (int): the tick that the enemy can do the next attack
alive (bool): flag indicating if the enemy is alive
awake (bool): flag indicating if the enemy is active
next_strike (float): time until the enemy's next action (in ms)
move_speed (float): speed of movement (in frames per grid)
offsetx, offsety (integer): steps moved from the center of the grid
spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning
wound (float): amount of wound
sfx_slash (Sound): sound effect indicating close-range attack damage
"""
def __init__(self, maze, x, y, color):
self.maze = maze
self.x, self.y = x, y
self.maze.map[x][y] = ENEMY
self.angle, self.color = pi / 4, color
self.awake = False
self.next_strike = 0
self.alive, self.awake = True, False
self.next_strike = 0.0
self.move_speed = self.maze.fps / ENEMY_SPEED
self.offsetx = self.offsety = 0
self.spin_speed = self.maze.fps / ENEMY_HP
self.spin_queue = self.wound = 0.0
self.sfx_slash = Sound(SFX_SLASH_HERO)
def get_pos(self):
"""Return coordinate of the center of the enemy."""
@property
def pos(self):
"""Coordinates (in pixels) of the center of the enemy."""
x, y = self.maze.get_pos(self.x, self.y)
step = self.maze.distance * HERO_SPEED / self.maze.fps
step = self.maze.distance * ENEMY_SPEED / self.maze.fps
return x + self.offsetx*step, y + self.offsety*step
@property
def distance(self):
"""Distance from the center of the enemy
to the center of the maze.
"""
return self.maze.get_distance(*self.pos)
def place(self, x=0, y=0):
"""Move the enemy by (x, y) (in grids)."""
self.x += x
self.y += y
self.maze.map[self.x][self.y] = ENEMY
if self.awake: self.maze.map[self.x][self.y] = ENEMY
@property
def spawn_volume(self):
"""Volumn of spawning sound effect."""
return 1 - self.distance / self.maze.get_distance(0, 0) / 2
def wake(self):
"""Wake the enemy up if it can see the hero.
@ -154,35 +210,30 @@ class Enemy:
has just woken it, False otherwise.
"""
if self.awake: return None
startx = starty = MIDDLE
stopx, stopy, distance = self.x, self.y, self.maze.distance
if startx > stopx: startx, stopx = stopx, startx
if starty > stopy: starty, stopy = stopy, starty
dx = (self.x-MIDDLE)*distance + self.maze.centerx - self.maze.x
dy = (self.y-MIDDLE)*distance + self.maze.centery - self.maze.y
mind = cosin(abs(atan(dy / dx)) if dx else 0) * distance
def get_distance(x, y): return abs(dy*x - dx*y) / (dy**2 + dx**2)**0.5
for i in range(startx, stopx + 1):
for j in range(starty, stopy + 1):
if self.maze.map[i][j] != WALL: continue
x, y = self.maze.get_pos(i, j)
if get_distance(x - self.maze.x, y - self.maze.y) <= mind:
return False
srcx, destx = self.x, MIDDLE
if abs(destx - srcx) != 1: srcx += sign(destx - srcx) or 1
srcy, desty = self.y, MIDDLE
if abs(desty - srcy) != 1: srcy += sign(desty - srcy) or 1
m, n = destx - srcx, desty - srcy
lcm = abs(m * n // gcd(m, n))
w, u = lcm // m, lcm // n
for i in range(lcm):
if self.maze.map[srcx+i//w][srcy+i//u] == WALL: return False
self.awake = True
play(self.maze.sfx_spawn, self.maze.get_distance(*self.get_pos()),
self.get_angle() + pi)
self.maze.map[self.x][self.y] = ENEMY
play(SFX_SPAWN, self.x, self.y)
return True
def fire(self):
"""Return True if the enemy has just fired, False otherwise."""
if self.maze.hero.dead: return False
x, y = self.get_pos()
x, y = self.pos
if (self.maze.get_distance(x, y) > FIRANGE*self.maze.distance
or get_ticks() < self.next_strike
or self.next_strike > 0
or (self.x, self.y) in AROUND_HERO or self.offsetx or self.offsety
or randrange((self.maze.hero.slashing+self.maze.isfast()+1) * 3)):
return False
self.next_strike = get_ticks() + ATTACK_SPEED
self.next_strike = ATTACK_SPEED
self.maze.bullets.append(
Bullet(self.maze.surface, x, y, self.get_angle() + pi, self.color))
return True
@ -195,13 +246,13 @@ class Enemy:
if self.offsety:
self.offsety -= sign(self.offsety)
return True
if get_ticks() < self.next_strike: return False
if self.next_strike > 0: return False
self.move_speed = self.maze.fps / speed
directions = [(sign(MIDDLE - self.x), 0), (0, sign(MIDDLE - self.y))]
shuffle(directions)
directions.append(choice(ADJACENT_GRIDS))
if self.maze.hero.dead: directions = choice(ADJACENT_GRIDS),
directions.append(choice(ADJACENTS))
if self.maze.hero.dead: directions = choice(ADJACENTS),
for x, y in directions:
if (x or y) and self.maze.map[self.x + x][self.y + y] == EMPTY:
self.offsetx = round(x * (1 - self.move_speed))
@ -213,92 +264,122 @@ class Enemy:
def get_slash(self):
"""Return the enemy's close-range damage."""
d = self.maze.slashd - self.maze.get_distance(*self.get_pos())
wound = d / self.maze.hero.R
wound = (self.maze.slashd - self.distance) / self.maze.hero.R
return wound if wound > 0 else 0.0
def slash(self):
"""Return the enemy's close-range damage per frame."""
wound = self.get_slash() / self.spin_speed
if self.spin_queue: self.maze.hit_hero(wound, self.color)
if self.spin_queue and wound: self.maze.hit_hero(wound, self.color)
return wound
def get_angle(self, reversed=False):
def get_angle(self):
"""Return the angle of the vector whose initial point is
the center of the screen and terminal point is the center of
the enemy.
"""
x, y = self.get_pos()
x, y = self.pos
return atan2(y - self.maze.y, x - self.maze.x)
def get_color(self):
"""Return current color of the enemy."""
return TANGO[self.color][int(self.wound)]
def isunnoticeable(self, x=None, y=None):
"""Return whether the enemy can be noticed.
Only search within column x and row y if these coordinates
are provided.
"""
if x is not None and self.x != x: return True
if y is not None and self.y != y: return True
return not self.awake or self.wound >= ENEMY_HP
def draw(self):
"""Draw the enemy."""
radious = self.maze.distance/SQRT2 - self.awake*2
square = regpoly(4, radious, self.angle, *self.get_pos())
color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
fill_aapolygon(self.maze.surface, square, color)
if self.isunnoticeable(): return
radius = self.maze.distance / SQRT2
square = regpoly(4, radius, self.angle, *self.pos)
fill_aapolygon(self.maze.surface, square, self.get_color())
def update(self):
"""Update the enemy."""
if self.awake:
self.spin_speed, tmp = self.maze.fps / ENEMY_HP, self.spin_speed
self.spin_queue *= self.spin_speed / tmp
self.next_strike -= 1000 / self.maze.fps
if not self.spin_queue and not self.fire() and not self.move():
self.spin_queue = randsign() * self.spin_speed
if not self.maze.hero.dead:
play(self.sfx_slash, self.get_slash(), self.get_angle())
if abs(self.spin_queue) > 0.5:
play(SFX_SLASH_HERO, self.x, self.y, self.get_slash())
if round(self.spin_queue) != 0:
self.angle += sign(self.spin_queue) * pi / 2 / self.spin_speed
self.spin_queue -= sign(self.spin_queue)
else:
self.angle, self.spin_queue = pi / 4, 0.0
if self.awake or get_ticks() >= self.maze.next_move: self.draw()
def hit(self, wound):
"""Handle the enemy when it's attacked."""
self.wound += wound
@property
def retired(self):
"""Provide compatibility with LockOn object."""
try:
return self._retired
except AttributeError:
return self.wound >= ENEMY_HP
@retired.setter
def retired(self, value):
self._retired = value
def die(self):
"""Handle the enemy's death."""
if self.awake:
self.maze.map[self.x][self.y] = EMPTY
if self.maze.enemy_weights[self.color] > MINW + 1.5:
self.maze.enemy_weights[self.color] -= 1.5
else:
self.maze.map[self.x][self.y] = WALL
self.maze.map[self.x][self.y] = EMPTY if self.wake else WALL
self.alive = False
class Chameleon(Enemy):
"""Object representing an enemy of Chameleon.
Additional attributes:
visible (int): the tick until which the Chameleon is visible
visible (float): time until the Chameleon is visible (in ms)
"""
def __init__(self, maze, x, y):
Enemy.__init__(self, maze, x, y, 'Chameleon')
self.visible = 0
super().__init__(maze, x, y, 'Chameleon')
self.visible = 0.0
def wake(self):
"""Wake the Chameleon up if it can see the hero."""
if Enemy.wake(self) is True:
self.visible = get_ticks() + 1000//ENEMY_SPEED
if super().wake() is True:
self.visible = 1000 / ENEMY_SPEED
def draw(self):
"""Draw the Chameleon."""
if (not self.awake or self.spin_queue
or get_ticks() < max(self.visible, self.maze.next_move)):
Enemy.draw(self)
def isunnoticeable(self, x=None, y=None):
"""Return whether the enemy can be noticed.
Only search within column x and row y if these coordinates
are provided.
"""
return (super().isunnoticeable(x, y)
or self.visible <= 0 and not self.spin_queue
and self.maze.next_move <= 0)
def update(self):
"""Update the Chameleon."""
super().update()
if self.awake: self.visible -= 1000 / self.maze.fps
def hit(self, wound):
"""Handle the Chameleon when it's attacked."""
self.visible = get_ticks() + 1000//ENEMY_SPEED
Enemy.hit(self, wound)
self.visible = 1000.0 / ENEMY_SPEED
super().hit(wound)
class Plum(Enemy):
"""Object representing an enemy of Plum."""
def __init__(self, maze, x, y):
Enemy.__init__(self, maze, x, y, 'Plum')
super().__init__(maze, x, y, 'Plum')
def clone(self, other):
"""Turn the other enemy into a clone of this Plum and return
@ -315,24 +396,24 @@ class Plum(Enemy):
class ScarletRed(Enemy):
"""Object representing an enemy of Scarlet Red."""
def __init__(self, maze, x, y):
Enemy.__init__(self, maze, x, y, 'ScarletRed')
super().__init__(maze, x, y, 'ScarletRed')
def fire(self):
"""Scarlet Red doesn't shoot."""
return False
def move(self):
return Enemy.move(self, ENEMY_SPEED * SQRT2)
return super().move(self, ENEMY_SPEED * SQRT2)
def slash(self):
"""Handle the Scarlet Red's close-range attack."""
self.wound -= Enemy.slash(self)
self.wound -= super().slash()
if self.wound < 0: self.wound = 0.0
def new_enemy(maze, x, y):
"""Return an enemy of a random type in the grid (x, y)."""
color = choices(maze.enemy_weights)
color = choice(ENEMIES)
try:
return getattr(modules[__name__], color)(maze, x, y)
except AttributeError:

View File

@ -1,65 +1,63 @@
# -*- coding: utf-8 -*-
# constants.py - module for shared constants
# This file is part of brutalmaze
# Copyright (C) 2017-2020 Nguyễn Gia Phong
#
# brutalmaze 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 file is part of Brutal Maze.
#
# brutalmaze is distributed in the hope that it will be useful,
# Brutal Maze is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Brutal Maze 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.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with brutalmaze. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__doc__ = 'brutalmaze module for shared constants'
__doc__ = 'Brutal Maze module for shared constants'
from pygame import image, K_UP, K_w, K_LEFT, K_a, K_DOWN, K_s, K_RIGHT, K_d
from pygame.mixer import Sound
from pkg_resources import resource_filename
from string import ascii_lowercase
ICON = image.load(resource_filename('brutalmaze', 'icon.png'))
MUSIC = resource_filename('brutalmaze', 'soundfx/music.ogg')
SFX_SPAWN = resource_filename('brutalmaze', 'soundfx/spawn.ogg')
SFX_SLASH_ENEMY = resource_filename('brutalmaze', 'soundfx/slash-enemy.ogg')
SFX_SLASH_HERO = resource_filename('brutalmaze', 'soundfx/slash-hero.ogg')
SFX_SHOT_ENEMY = resource_filename('brutalmaze', 'soundfx/shot-enemy.ogg')
SFX_SHOT_HERO = resource_filename('brutalmaze', 'soundfx/shot-hero.ogg')
SFX_MISSED = resource_filename('brutalmaze', 'soundfx/missed.ogg')
SFX_HEART = resource_filename('brutalmaze', 'soundfx/heart.ogg')
SFX_LOSE = resource_filename('brutalmaze', 'soundfx/lose.ogg')
import pygame
from pkg_resources import resource_filename as pkg_file
UP = (K_UP, K_w)
LEFT = (K_LEFT, K_a)
DOWN = (K_DOWN, K_s)
RIGHT = (K_RIGHT, K_d)
SETTINGS = pkg_file('brutalmaze', 'settings.ini')
ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
SFX_NOISE = pkg_file('brutalmaze', 'soundfx/noise.ogg')
SFX_SPAWN = pkg_file('brutalmaze', 'soundfx/spawn.ogg')
SFX_SLASH_ENEMY = pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg')
SFX_SLASH_HERO = pkg_file('brutalmaze', 'soundfx/slash-hero.ogg')
SFX_SHOT_ENEMY = pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg')
SFX_SHOT_HERO = pkg_file('brutalmaze', 'soundfx/shot-hero.ogg')
SFX_MISSED = pkg_file('brutalmaze', 'soundfx/missed.ogg')
SFX_HEART = pkg_file('brutalmaze', 'soundfx/heart.ogg')
SFX_LOSE = pkg_file('brutalmaze', 'soundfx/lose.ogg')
SFX = (SFX_NOISE, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_SLASH_HERO,
SFX_SHOT_ENEMY, SFX_SHOT_HERO, SFX_MISSED, SFX_HEART, SFX_LOSE)
SQRT2 = 2 ** 0.5
INIT_SCORE = 5**0.5/2 + 0.5 # golden mean
INIT_FPS = 30.0
MAX_FPS = 144.0
SIZE = 640, 480
MAZE_SIZE = 10
ROAD_WIDTH = 5 # grids
CELL_WIDTH = ROAD_WIDTH * 2 # grids
MIDDLE = (MAZE_SIZE + MAZE_SIZE%2 - 1)*ROAD_WIDTH + ROAD_WIDTH//2
LAST_ROW = (MAZE_SIZE-1) * ROAD_WIDTH * 2
INIT_SCORE = 2
ROAD_WIDTH = 3 # grids
WALL_WIDTH = 4 # grids
CELL_WIDTH = WALL_WIDTH + ROAD_WIDTH*2 # grids
CELL_NODES = ROAD_WIDTH, ROAD_WIDTH + WALL_WIDTH, 0
MAZE_SIZE = 10 # cells
MIDDLE = MAZE_SIZE // 2 * CELL_WIDTH
HEAL_SPEED = 1 # HP/s
HERO_SPEED = 5 # grid/s
ENEMY_SPEED = 6 # grid/s
ENEMY_SPEED = 6 # grid/s
BULLET_SPEED = 15 # grid/s
ATTACK_SPEED = 333 # ms/strike
ATTACK_SPEED = 333.333 # ms/strike
MAX_WOUND = 1 # per attack turn
FIRANGE = 6 # grids
BULLET_LIFETIME = 1000.0 * FIRANGE / (BULLET_SPEED-HERO_SPEED) # ms
BULLET_LIFETIME = 1000 * FIRANGE / (BULLET_SPEED-HERO_SPEED) # ms
EMPTY, WALL, HERO, ENEMY = range(4)
ADJACENT_GRIDS = (1, 0), (0, 1), (-1, 0), (0, -1)
AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in
ADJACENT_GRIDS + ((1, 1), (-1, 1), (-1, -1), (1, -1)))
ADJACENTS = (1, 0), (0, 1), (-1, 0), (0, -1)
CORNERS = (1, 1), (-1, 1), (-1, -1), (1, -1)
AROUND_HERO = set((MIDDLE + x, MIDDLE + y) for x, y in ADJACENTS + CORNERS)
TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)),
@ -70,11 +68,16 @@ TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
'ScarletRed': ((239, 41, 41), (204, 0, 0), (164, 0, 0)),
'Aluminium': ((238, 238, 236), (211, 215, 207), (186, 189, 182),
(136, 138, 133), (85, 87, 83), (46, 52, 54))}
TANGO_VALUES = list(TANGO.values())
ENEMIES = ['Butter', 'Orange', 'Chocolate', 'Chameleon',
'SkyBlue', 'Plum', 'ScarletRed']
MINW, MAXW = 24, 36
COLOR_CODE = ascii_lowercase + '0'
COLORS = {c: COLOR_CODE[i] for i, c in enumerate(
color for code in ENEMIES + ['Aluminium'] for color in TANGO[code])}
ENEMY_HP = 3
HERO_HP = 5
MIN_BEAT = 526
MIN_BEAT = 420
BG_COLOR = TANGO['Aluminium'][-1]
FG_COLOR = TANGO['Aluminium'][0]
JSON_SEPARATORS = ',', ':'

430
brutalmaze/game.py Normal file
View File

@ -0,0 +1,430 @@
# game.py - main module, starts game and main loop
# Copyright (C) 2017-2020 Nguyễn Gia Phong
#
# This file is part of Brutal Maze.
#
# Brutal Maze is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Brutal Maze 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__version__ = '0.9.4'
import re
from argparse import ArgumentParser, FileType, RawTextHelpFormatter
from configparser import ConfigParser
from contextlib import redirect_stdout
from io import StringIO
from math import atan2, pi, radians
from os.path import join as pathjoin, pathsep
from socket import SO_REUSEADDR, SOL_SOCKET, socket
from sys import stdout
from threading import Thread
with redirect_stdout(StringIO()): import pygame
from appdirs import AppDirs
from palace import Context, Device, free, use_context
from pygame import KEYDOWN, MOUSEBUTTONUP, QUIT, VIDEORESIZE
from pygame.time import Clock, get_ticks
from .constants import HERO_SPEED, ICON, MIDDLE, SETTINGS, SFX, SFX_NOISE
from .maze import Maze
from .misc import deg, join, play, sign
class ConfigReader:
"""Object reading and processing INI configuration file for
Brutal Maze.
"""
CONTROL_ALIASES = (('New game', 'new'), ('Toggle pause', 'pause'),
('Toggle mute', 'mute'),
('Move left', 'left'), ('Move right', 'right'),
('Move up', 'up'), ('Move down', 'down'),
('Long-range attack', 'shot'),
('Close-range attack', 'slash'))
WEIRD_MOUSE_ERR = '{}: Mouse is not a suitable control'
INVALID_CONTROL_ERR = '{}: {} is not recognized as a valid control key'
def __init__(self, filenames):
self.config = ConfigParser()
self.config.read(SETTINGS) # default configuration
self.config.read(filenames)
# Fallback to None when attribute is missing
def __getattr__(self, name): return None
def parse(self):
"""Parse configurations."""
self.size = (self.config.getint('Graphics', 'Screen width'),
self.config.getint('Graphics', 'Screen height'))
self.max_fps = self.config.getint('Graphics', 'Maximum FPS')
self.muted = self.config.getboolean('Sound', 'Muted')
self.musicvol = self.config.getfloat('Sound', 'Music volume')
self.touch = self.config.getboolean('Control', 'Touch')
self.export_dir = self.config.get('Record', 'Directory')
self.export_rate = self.config.getint('Record', 'Frequency')
self.server = self.config.getboolean('Server', 'Enable')
self.host = self.config.get('Server', 'Host')
self.port = self.config.getint('Server', 'Port')
self.timeout = self.config.getfloat('Server', 'Timeout')
self.headless = self.config.getboolean('Server', 'Headless')
if self.server: return
self.key, self.mouse = {}, {}
for cmd, alias in self.CONTROL_ALIASES:
i = self.config.get('Control', cmd)
if re.match('mouse[1-3]$', i.lower()):
if alias not in ('shot', 'slash'):
raise ValueError(self.WEIRD_MOUSE_ERR.format(cmd))
self.mouse[alias] = int(i[-1]) - 1
continue
if len(i) == 1:
self.key[alias] = ord(i.lower())
continue
try:
self.key[alias] = getattr(pygame, 'K_{}'.format(i.upper()))
except AttributeError:
raise ValueError(self.INVALID_CONTROL_ERR.format(cmd, i))
def read_args(self, arguments):
"""Read and parse a ArgumentParser.Namespace."""
for option in ('size', 'max_fps', 'muted', 'musicvol',
'touch', 'export_dir', 'export_rate', 'server',
'host', 'port', 'timeout', 'headless'):
value = getattr(arguments, option)
if value is not None: setattr(self, option, value)
class Game:
"""Object handling main loop and IO."""
def __init__(self, config: ConfigReader):
pygame.init()
self.headless = config.headless and config.server
if not self.headless: pygame.display.set_icon(ICON)
self.actx = None if self.headless else Context(Device())
self._mute = config.muted
if config.server:
self.server = socket()
self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.server.bind((config.host, config.port))
self.server.listen(1)
print('Socket server is listening on {}:{}'.format(config.host,
config.port))
self.timeout = config.timeout
self.sockinp = 0, 0, -pi * 3 / 4, 0, 0 # freeze and point to NW
else:
self.server = self.sockinp = None
self.max_fps, self.fps = config.max_fps, config.max_fps
self.musicvol = config.musicvol
self.touch = config.touch
self.key, self.mouse = config.key, config.mouse
self.maze = Maze(config.max_fps, config.size, config.headless,
config.export_dir, 1000 / config.export_rate)
self.hero = self.maze.hero
self.clock, self.paused = Clock(), False
def __enter__(self):
if self.actx is not None:
use_context(self.actx)
self.actx.listener.position = MIDDLE, -MIDDLE, 0
self.actx.listener.gain = not self._mute
self._source = play(SFX_NOISE)
self._source.looping = True
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.server is not None: self.server.close()
if not self.hero.dead: self.maze.dump_records()
if self.actx is not None:
free(SFX)
self._source.stop()
self.actx.update()
use_context(None)
self.actx.destroy()
self.actx.device.close()
pygame.quit()
@property
def mute(self):
"""Mute state."""
return getattr(self, '_mute', 1)
@mute.setter
def mute(self, value):
"""Mute state."""
self._mute = int(bool(value))
self.actx.listener.gain = not self._mute
def export_txt(self):
"""Export maze data to string."""
export = self.maze.update_export(forced=True)
return '{} {} {} {}\n{}{}{}{}'.format(
len(export['m']), len(export['e']), len(export['b']), export['s'],
''.join(row + '\n' for row in export['m']), join(export['h']),
''.join(map(join, export['e'])), ''.join(map(join, export['b'])))
def update(self):
"""Draw and handle meta events on Pygame window.
Return False if QUIT event is captured, True otherwise.
"""
events = pygame.event.get()
for event in events:
if event.type == QUIT:
return False
elif event.type == VIDEORESIZE:
self.maze.resize((event.w, event.h))
elif event.type == KEYDOWN:
if event.key == self.key['mute']:
self.mute ^= 1
elif not self.server:
if event.key == self.key['new']:
self.maze.reinit()
elif event.key == self.key['pause'] and not self.hero.dead:
self.paused ^= True
elif event.type == MOUSEBUTTONUP and self.touch:
# We're careless about which mouse button is clicked.
maze = self.maze
if self.hero.dead:
maze.reinit()
else:
x, y = pygame.mouse.get_pos()
maze.destx, maze.desty = maze.get_grid(x, y)
if maze.set_step(maze.isdisplayed):
maze.target = maze.get_target(x, y)
self.hero.firing = not maze.target.retired
if maze.stepx == maze.stepy == 0:
maze.destx = maze.desty = MIDDLE
# Compare current FPS with the average of the last 10 frames
new_fps = self.clock.get_fps()
if new_fps < self.fps:
self.fps -= 1
elif self.fps < self.max_fps and not self.paused:
self.fps += 5
if not self.paused: self.maze.update(self.fps)
if not self.headless: self.maze.draw()
self.clock.tick(self.fps)
self.actx.update()
return True
def move(self, x=0, y=0):
"""Command the hero to move faster in the given direction."""
maze = self.maze
velocity = maze.distance * HERO_SPEED / self.fps
accel = velocity * HERO_SPEED / self.fps
if x == y == 0:
maze.set_step()
x, y = maze.stepx, maze.stepy
else:
x, y = -x, -y # or move the maze in the reverse direction
if maze.next_move > 0 or not x:
maze.vx -= sign(maze.vx) * accel
if abs(maze.vx) < accel * 2: maze.vx = 0.0
elif x * maze.vx < 0:
maze.vx += x * 2 * accel
else:
maze.vx += x * accel
if abs(maze.vx) > velocity: maze.vx = x * velocity
if maze.next_move > 0 or not y:
maze.vy -= sign(maze.vy) * accel
if abs(maze.vy) < accel * 2: maze.vy = 0.0
elif y * maze.vy < 0:
maze.vy += y * 2 * accel
else:
maze.vy += y * accel
if abs(maze.vy) > velocity: maze.vy = y * velocity
def control(self, x, y, angle, firing, slashing):
"""Control how the hero move and attack."""
self.move(x, y)
self.hero.update_angle(angle)
self.hero.firing = firing
self.hero.slashing = slashing
def remote_control(self):
"""Handle remote control though socket server.
This function is supposed to be run in a Thread.
"""
clock = Clock()
while True:
connection, address = self.server.accept()
connection.settimeout(self.timeout)
time = get_ticks()
print('[{}] Connected to {}:{}'.format(time, *address))
self.maze.reinit()
while True:
if self.hero.dead:
connection.send('0000000'.encode())
break
data = self.export_txt().encode()
alpha = deg(self.hero.angle)
connection.send('{:07}'.format(len(data)).encode())
connection.send(data)
try:
buf = connection.recv(7)
except: # noqa
break # client is closed or timed out
if not buf: break
try:
move, angle, attack = map(int, buf.decode().split())
except ValueError: # invalid input
break
y, x = (i - 1 for i in divmod(move, 3))
# Time is the essence.
angle = self.hero.angle if angle == alpha else radians(angle)
self.sockinp = x, y, angle, attack & 1, attack >> 1
clock.tick(self.fps)
self.sockinp = 0, 0, -pi * 3 / 4, 0, 0
new_time = get_ticks()
print('[{0}] {3}:{4} scored {1} points in {2}ms'.format(
new_time, self.maze.get_score(), new_time - time, *address))
connection.close()
if not self.hero.dead: self.maze.lose()
def touch_control(self):
"""Handle touch control."""
maze, hero = self.maze, self.hero
if maze.target.retired: hero.firing = False
if hero.firing:
x, y = maze.get_pos(maze.target.x, maze.target.y)
else:
x, y = pygame.mouse.get_pos()
hero.update_angle(atan2(y - hero.y, x - hero.x))
self.move()
def user_control(self):
"""Handle direct control from user's mouse and keyboard."""
if self.hero.dead: return
keys = pygame.key.get_pressed()
buttons = pygame.mouse.get_pressed()
right = keys[self.key['right']] - keys[self.key['left']]
down = keys[self.key['down']] - keys[self.key['up']]
x, y = pygame.mouse.get_pos()
angle = atan2(y - self.hero.y, x - self.hero.x)
try:
firing = keys[self.key['shot']]
except KeyError:
firing = buttons[self.mouse['shot']]
try:
slashing = keys[self.key['slash']]
except KeyError:
slashing = buttons[self.mouse['slash']]
self.control(right, down, angle, firing, slashing)
def main():
"""Start game and main loop."""
# Read configuration file
dirs = AppDirs(appname='brutalmaze', appauthor=False, multipath=True)
parents = dirs.site_config_dir.split(pathsep)
parents.append(dirs.user_config_dir)
filenames = [pathjoin(parent, 'settings.ini') for parent in parents]
config = ConfigReader(filenames)
config.parse()
# Parse command-line arguments
parser = ArgumentParser(usage='%(prog)s [options]',
formatter_class=RawTextHelpFormatter)
parser.add_argument('-v', '--version', action='version',
version='Brutal Maze {}'.format(__version__))
parser.add_argument(
'--write-config', nargs='?', const=stdout, type=FileType('w'),
metavar='PATH', dest='defaultcfg',
help='write default config and exit, if PATH not specified use stdout')
parser.add_argument(
'-c', '--config', metavar='PATH',
help='location of the configuration file (fallback: {})'.format(
pathsep.join(filenames)))
parser.add_argument(
'-s', '--size', type=int, nargs=2, metavar=('X', 'Y'),
help='the desired screen size (fallback: {}x{})'.format(*config.size))
parser.add_argument(
'-f', '--max-fps', type=int, metavar='FPS',
help='the desired maximum FPS (fallback: {})'.format(config.max_fps))
parser.add_argument(
'--mute', '-m', action='store_true', default=None, dest='muted',
help='mute all sounds (fallback: {})'.format(config.muted))
parser.add_argument('--unmute', action='store_false', dest='muted',
help='unmute sound')
parser.add_argument(
'--music-volume', type=float, metavar='VOL', dest='musicvol',
help='between 0.0 and 1.0 (fallback: {})'.format(config.musicvol))
parser.add_argument(
'--touch', action='store_true', default=None,
help='enable touch-friendly control (fallback: {})'.format(
config.touch))
parser.add_argument('--no-touch', action='store_false', dest='touch',
help='disable touch-friendly control')
parser.add_argument(
'--record-dir', metavar='DIR', dest='export_dir',
help='directory to write game records (fallback: {})'.format(
config.export_dir or '*disabled*'))
parser.add_argument(
'--record-rate', metavar='SPF', dest='export_rate',
help='snapshots of game state per second (fallback: {})'.format(
config.export_rate))
parser.add_argument(
'--server', action='store_true', default=None,
help='enable server (fallback: {})'.format(config.server))
parser.add_argument('--no-server', action='store_false', dest='server',
help='disable server')
parser.add_argument(
'--host', help='host to bind server to (fallback: {})'.format(
config.host))
parser.add_argument(
'--port', type=int,
help='port for server to listen on (fallback: {})'.format(config.port))
parser.add_argument(
'-t', '--timeout', type=float,
help='socket operations timeout in seconds (fallback: {})'.format(
config.timeout))
parser.add_argument(
'--head', action='store_false', default=None, dest='headless',
help='run server with graphics and sound (fallback: {})'.format(
not config.headless))
parser.add_argument('--headless', action='store_true',
help='run server without graphics or sound')
args = parser.parse_args()
if args.defaultcfg is not None:
with open(SETTINGS) as settings: args.defaultcfg.write(settings.read())
args.defaultcfg.close()
exit()
# Manipulate config
if args.config:
config.config.read(args.config)
config.parse()
config.read_args(args)
# Main loop
with Game(config) as game:
if config.server:
socket_thread = Thread(target=game.remote_control)
socket_thread.daemon = True # make it disposable
socket_thread.start()
while game.update(): game.control(*game.sockinp)
elif config.touch:
while game.update(): game.touch_control()
else:
while game.update(): game.user_control()
# Allow launching the game via invoking ``python -m brutalmaze.game''
if __name__ == '__main__': main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
# main.py - main module, starts game and main loop
# This file is part of brutalmaze
#
# brutalmaze 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.
#
# brutalmaze 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 brutalmaze. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong
from collections import deque
import pygame
from pygame.locals import *
from .constants import *
from .maze import Maze
from .misc import some
def main():
"""Start game and main loop."""
pygame.mixer.pre_init(frequency=44100)
pygame.init()
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.play(-1)
pygame.display.set_icon(ICON)
pygame.fastevent.init()
maze, clock = Maze(SIZE, INIT_FPS), pygame.time.Clock()
fps, flash_time, going = INIT_FPS, deque(), True
while going:
events = pygame.fastevent.get()
for event in events:
if event.type == QUIT:
going = False
elif event.type == VIDEORESIZE:
maze.resize(event.w, event.h)
elif event.type == KEYDOWN:
if event.key == K_F2: # new game
maze.__init__((maze.w, maze.h), fps)
elif event.key in (K_ESCAPE, K_p) and not maze.hero.dead:
maze.paused ^= True
if not maze.hero.dead:
keys = pygame.key.get_pressed()
buttons = pygame.mouse.get_pressed()
maze.move(some(keys, LEFT) - some(keys, RIGHT),
some(keys, UP) - some(keys, DOWN), fps)
maze.hero.slashing = keys[K_RETURN] or buttons[2]
maze.hero.firing = buttons[0]
if len(flash_time) > 5:
new_fps = 5000.0 / (flash_time[-1] - flash_time[0])
flash_time.popleft()
if new_fps < fps:
fps -= 1
elif fps < MAX_FPS and not maze.paused:
fps += 5
maze.update(fps)
flash_time.append(pygame.time.get_ticks())
clock.tick(fps)
pygame.quit()

View File

@ -1,117 +1,155 @@
# -*- coding: utf-8 -*-
# maze.py - module for the maze class
# This file is part of brutalmaze
# Copyright (C) 2017-2020 Nguyễn Gia Phong
#
# brutalmaze 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 file is part of Brutal Maze.
#
# brutalmaze is distributed in the hope that it will be useful,
# Brutal Maze is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Brutal Maze 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.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with brutalmaze. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__doc__ = 'brutalmaze module for the maze class'
__doc__ = 'Brutal Maze module for the maze class'
from collections import deque
from math import pi, log
from random import choice, getrandbits, uniform
import json
from collections import defaultdict, deque
from math import log, pi
from os import path
from random import choice, sample
import pygame
from pygame import RESIZABLE
from pygame.mixer import Sound
from pygame.time import get_ticks
from .characters import Hero, new_enemy
from .constants import *
from .misc import round2, sign, regpoly, fill_aapolygon, play
from .weapons import Bullet
def new_cell(bit, upper=True):
"""Return a half of a cell of the maze based on the given bit."""
if bit: return deque([WALL]*ROAD_WIDTH + [EMPTY]*ROAD_WIDTH)
if upper: return deque([WALL] * (ROAD_WIDTH<<1))
return deque([EMPTY] * (ROAD_WIDTH<<1))
def new_column():
"""Return a newly generated column of the maze."""
column = deque()
upper, lower = deque(), deque()
for _ in range(MAZE_SIZE):
b = getrandbits(1)
upper.extend(new_cell(b))
lower.extend(new_cell(b, False))
for _ in range(ROAD_WIDTH): column.append(upper.__copy__())
for _ in range(ROAD_WIDTH): column.append(lower.__copy__())
return column
from .constants import (ADJACENTS, ATTACK_SPEED, BG_COLOR,
BULLET_LIFETIME, CELL_NODES, CELL_WIDTH, COLORS,
EMPTY, ENEMIES, ENEMY, ENEMY_HP, FG_COLOR, HERO,
HERO_HP, HERO_SPEED, INIT_SCORE, JSON_SEPARATORS,
MAX_WOUND, MAZE_SIZE, MIDDLE, ROAD_WIDTH,
SFX_LOSE, SFX_MISSED, SFX_SLASH_ENEMY, SFX_SPAWN,
SQRT2, TANGO_VALUES, WALL, WALL_WIDTH)
from .misc import around, deg, fill_aapolygon, json_rec, play, regpoly, sign
from .weapons import LockOn
class Maze:
"""Object representing the maze, including the characters.
Attributes:
w, h: width and height of the display
fps: current frame rate
w, h (int): width and height of the display (in px)
fps (float): current frame rate
surface (pygame.Surface): the display to draw on
distance (float): distance between centers of grids (in px)
x, y (int): coordinates of the center of the hero (in px)
centerx, centery (float): center grid's center's coordinates (in px)
rangex, rangey: range of the index of the grids on display
paused (bool): flag indicates if the game is paused
rangex, rangey (list): range of the index of the grids on display
score (float): current score
map (deque of deque): map of grids representing objects on the maze
vx, vy (float): velocity of the maze movement (in pixels per frame)
rotatex, rotatey: grids rotated
bullets (list of Bullet): bullets flying
enemy_weights (dict): probabilities of enemies to be created
rotatex, rotatey (int): grids rotated
bullets (list of .weapons.Bullet): flying bullets
enemies (list of Enemy): alive enemies
hero (Hero): the hero
next_move (int): the tick that the hero gets mobilized
next_slashfx (int): the tick to play next slash effect of the hero
destx, desty (int): the grid the hero is moving to
stepx, stepy (int): direction the maze is moving
target (Enemy or LockOn): target to automatically aim at
next_move (float): time until the hero gets mobilized (in ms)
glitch (float): time that the maze remain flashing colors (in ms)
next_slashfx (float): time until next slash effect of the hero (in ms)
slashd (float): minimum distance for slashes to be effective
sfx_slash (Sound): sound effect indicating an enemy get slashed
sfx_shot (Sound): sound effect indicating an enemy get shot
sfx_lose (Sound): sound effect to be played when you lose
export (list of defaultdict): records of game states
export_dir (str): directory containing records of game states
export_rate (float): milliseconds per snapshot
next_export (float): time until next snapshot (in ms)
"""
def __init__(self, size, fps):
self.w, self.h = size
def __init__(self, fps, size, headless, export_dir, export_rate):
self.fps = fps
self.surface = pygame.display.set_mode(size, RESIZABLE)
self.w, self.h = size
if headless:
self.surface = None
else:
self.surface = pygame.display.set_mode(size, pygame.RESIZABLE)
self.export_dir = path.abspath(export_dir) if export_dir else ''
self.next_export = self.export_rate = export_rate
self.export = []
self.distance = (self.w * self.h / 416) ** 0.5
self.x, self.y = self.w // 2, self.h // 2
self.centerx, self.centery = self.w / 2.0, self.h / 2.0
w, h = (int(i/self.distance/2 + 2) for i in size)
self.rangex = range(MIDDLE - w, MIDDLE + w + 1)
self.rangey = range(MIDDLE - h, MIDDLE + h + 1)
self.paused, self.score = False, INIT_SCORE
self.centerx, self.centery = self.w / 2, self.h / 2
w, h = (int(i/self.distance/2 + 1) for i in size)
self.rangex = list(range(MIDDLE - w, MIDDLE + w + 1))
self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
self.score = INIT_SCORE
self.new_map()
self.map = deque()
for _ in range(MAZE_SIZE): self.map.extend(new_column())
self.vx = self.vy = 0.0
self.rotatex = self.rotatey = 0
self.bullets, self.enemies = [], []
self.enemy_weights = {color: MINW for color in ENEMIES}
self.add_enemy()
self.hero = Hero(self.surface, fps)
self.map[MIDDLE][MIDDLE] = HERO
self.next_move = self.next_slashfx = 0
self.hero = Hero(self.surface, fps, size)
self.target = LockOn(MIDDLE, MIDDLE, retired=True)
self.next_move = self.glitch = self.next_slashfx = 0.0
self.slashd = self.hero.R + self.distance/SQRT2
self.sfx_spawn = Sound(SFX_SPAWN)
self.sfx_slash = Sound(SFX_SLASH_ENEMY)
self.sfx_shot = Sound(SFX_SHOT_ENEMY)
self.sfx_lose = Sound(SFX_LOSE)
self.sfx_spawn = SFX_SPAWN
self.sfx_slash = SFX_SLASH_ENEMY
self.sfx_lose = SFX_LOSE
def new_cell(self, x, y):
"""Draw on the map a newly created cell
whose coordinates are given.
"""
def draw_bit(bit, dx=0, dy=0):
startx, starty = x + CELL_NODES[dx], y + CELL_NODES[dy]
height = ROAD_WIDTH if dy else WALL_WIDTH
for i in range(ROAD_WIDTH if dx else WALL_WIDTH):
for j in range(height): self.map[startx + i][starty + j] = bit
x, y = x * CELL_WIDTH, y * CELL_WIDTH
draw_bit(WALL)
walls = set(sample(ADJACENTS, 2))
walls.add(choice(ADJACENTS))
for i, j in ADJACENTS:
draw_bit((WALL if (i, j) in walls else EMPTY), i, j)
def isdisplayed(self, x, y):
"""Return True if the grid (x, y) is in the displayable part
of the map, False otherwise.
"""
return (self.rangex[0] <= x <= self.rangex[-1]
and self.rangey[0] <= y <= self.rangey[-1])
def new_map(self):
"""Generate a new map."""
self.map = deque(deque(EMPTY for _ in range(MAZE_SIZE * CELL_WIDTH))
for _ in range(MAZE_SIZE * CELL_WIDTH))
for x in range(MAZE_SIZE):
for y in range(MAZE_SIZE): self.new_cell(x, y)
# Regenerate if the hero is trapped. This can only reach
# maximum recursion depth is there's a flaw with the system's entropy.
room, visited = [(MIDDLE, MIDDLE)], set()
while room:
bit = room.pop()
if bit not in visited:
if not self.isdisplayed(*bit): break
visited.add(bit)
for x, y in around(*bit):
if self.map[x][y] == EMPTY: room.append((x, y))
else:
self.new_map()
self.map[MIDDLE][MIDDLE] = HERO
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
def add_enemy(self):
"""Add enough enemies."""
self.enemies = [e for e in self.enemies if e.alive]
walls = [(i, j) for i in self.rangex for j in self.rangey
if self.map[i][j] == WALL]
plums = [e for e in self.enemies if e.color == 'Plum' and e.awake]
@ -119,77 +157,105 @@ class Maze:
num = log(self.score, INIT_SCORE)
while walls and len(self.enemies) < num:
x, y = choice(walls)
if all(self.map[x + a][y + b] == WALL for a, b in ADJACENT_GRIDS):
if all(self.map[x + a][y + b] == WALL for a, b in ADJACENTS):
continue
enemy = new_enemy(self, x, y)
self.enemies.append(enemy)
if plum is None or not plum.clone(enemy):
walls.remove((x, y))
else:
self.map[x][y] = WALL
if plum is None or not plum.clone(enemy): walls.remove((x, y))
def get_pos(self, x, y):
"""Return coordinate of the center of the grid (x, y)."""
return (self.centerx + (x - MIDDLE)*self.distance,
self.centery + (y - MIDDLE)*self.distance)
def get_grid(self, x, y):
"""Return the grid containing the point (x, y)."""
return (MIDDLE + round((x-self.centerx) / self.distance),
MIDDLE + round((y-self.centery) / self.distance))
def get_target(self, x, y):
"""Return shooting target the grid containing the point (x, y).
If the grid is the hero, return a retired target.
"""
gridx, gridy = self.get_grid(x, y)
if gridx == gridy == MIDDLE: return LockOn(gridx, gridy, True)
for enemy in self.enemies:
if not enemy.isunnoticeable(gridx, gridy): return enemy
return LockOn(gridx, gridy)
def get_score(self):
"""Return the current score."""
return int(self.score - INIT_SCORE)
def get_color(self):
"""Return color of a grid."""
return choice(TANGO_VALUES)[0] if self.glitch > 0 else FG_COLOR
def draw(self):
"""Draw the maze."""
self.surface.fill(BG_COLOR)
if get_ticks() < self.next_move: return
for i in self.rangex:
for j in self.rangey:
if self.map[i][j] != WALL: continue
x, y = self.get_pos(i, j)
square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
fill_aapolygon(self.surface, square, FG_COLOR)
if self.next_move <= 0:
for i in self.rangex:
for j in self.rangey:
if self.map[i][j] != WALL: continue
x, y = self.get_pos(i, j)
square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
fill_aapolygon(self.surface, square, self.get_color())
for enemy in self.enemies: enemy.draw()
if not self.hero.dead: self.hero.draw()
bullet_radius = self.distance / 4
for bullet in self.bullets: bullet.draw(bullet_radius)
pygame.display.flip()
pygame.display.set_caption(
'Brutal Maze - Score: {}'.format(self.get_score()))
def rotate(self):
"""Rotate the maze if needed."""
x = int((self.centerx-self.x) * 2 / self.distance)
y = int((self.centery-self.y) * 2 / self.distance)
if x == y == 0: return
for enemy in self.enemies: self.map[enemy.x][enemy.y] = EMPTY
for enemy in self.enemies:
if self.map[enemy.x][enemy.y] == ENEMY:
self.map[enemy.x][enemy.y] = EMPTY
self.map[MIDDLE][MIDDLE] = EMPTY
if x:
self.centerx -= x * self.distance
self.map.rotate(x)
self.rotatex += x
if y:
self.centery -= y * self.distance
for d in self.map: d.rotate(y)
self.rotatey += y
self.centerx -= x * self.distance
self.map.rotate(x)
self.rotatex += x
self.centery -= y * self.distance
for d in self.map: d.rotate(y)
self.rotatey += y
self.map[MIDDLE][MIDDLE] = HERO
if self.map[self.destx][self.desty] != HERO:
self.destx += x
self.desty += y
self.stepx = self.stepy = 0
# Respawn the enemies that fall off the display
killist = []
for i, enemy in enumerate(self.enemies):
enemy.place(x, y)
if enemy.x not in self.rangex or enemy.y not in self.rangey:
if not self.isdisplayed(enemy.x, enemy.y):
self.score += enemy.wound
enemy.die()
killist.append(i)
for i in reversed(killist): self.enemies.pop(i)
self.add_enemy()
# LockOn target is not yet updated.
if isinstance(self.target, LockOn):
self.target.place(x, y, self.isdisplayed)
# Regenerate the maze
if abs(self.rotatex) == CELL_WIDTH:
self.rotatex = 0
for _ in range(CELL_WIDTH): self.map.pop()
self.map.extend(new_column())
for i in range(-CELL_WIDTH, 0):
self.map[i].rotate(self.rotatey)
for i in range(CELL_WIDTH): self.map[i].rotate(-self.rotatey)
for i in range(MAZE_SIZE): self.new_cell(0, i)
for i in range(CELL_WIDTH): self.map[i].rotate(self.rotatey)
if abs(self.rotatey) == CELL_WIDTH:
self.rotatey = 0
for i in range(MAZE_SIZE):
b, c = getrandbits(1), (i-1)*CELL_WIDTH + self.rotatex
for j, grid in enumerate(new_cell(b)):
for k in range(ROAD_WIDTH):
self.map[c + k][LAST_ROW + j] = grid
c += ROAD_WIDTH
for j, grid in enumerate(new_cell(b, False)):
for k in range(ROAD_WIDTH):
self.map[c + k][LAST_ROW + j] = grid
self.map.rotate(-self.rotatex)
for i in range(MAZE_SIZE): self.new_cell(i, 0)
self.map.rotate(self.rotatex)
def get_distance(self, x, y):
"""Return the distance from the center of the maze to the point
@ -199,78 +265,84 @@ class Maze:
def hit_hero(self, wound, color):
"""Handle the hero when he loses HP."""
fx = (uniform(0, sum(self.enemy_weights.values()))
< self.enemy_weights[color])
time = get_ticks()
if (color == 'Butter' or color == 'ScarletRed') and fx:
self.hero.wound += wound * 2.5
elif color == 'Orange' and fx:
self.hero.next_heal = max(self.hero.next_heal, time) + wound*1000
elif color == 'SkyBlue' and fx:
self.next_move = max(self.next_move, time) + wound*1000
else:
self.hero.wound += wound
if self.enemy_weights[color] + wound < MAXW:
self.enemy_weights[color] += wound
if self.hero.wound > HERO_HP and not self.hero.dead: self.lose()
if color == 'Orange':
# If called by close-range attack, this is FPS-dependant, although
# in playable FPS (24 to infinity), the difference within 2%.
self.hero.next_heal = abs(self.hero.next_heal * (1 - wound))
elif choice(ENEMIES) == color:
self.hero.next_heal = -1.0 # what doesn't kill you heals you
if color == 'Butter' or color == 'ScarletRed':
wound *= ENEMY_HP
elif color == 'Chocolate':
self.hero.highness += wound
wound = 0
elif color == 'SkyBlue':
self.next_move = max(self.next_move, 0) + wound*1000
wound = 0
if wound and sum(self.hero.wounds) < MAX_WOUND:
self.hero.wounds[-1] += wound
def slash(self):
"""Handle close-range attacks."""
for enemy in self.enemies: enemy.slash()
if not self.hero.spin_queue: return
killist = []
for i, enemy in enumerate(self.enemies):
d = self.slashd - self.get_distance(*enemy.get_pos())
for enemy in filter(lambda e: e.awake, self.enemies):
d = self.slashd - enemy.distance
if d > 0:
wound, time = d * SQRT2 / self.distance, get_ticks()
if time >= self.next_slashfx:
play(self.sfx_slash, wound, enemy.get_angle())
self.next_slashfx = time + ATTACK_SPEED
wound = d * SQRT2 / self.distance
if self.next_slashfx <= 0:
play(SFX_SLASH_ENEMY, enemy.x, enemy.y, wound)
self.next_slashfx = ATTACK_SPEED
enemy.hit(wound / self.hero.spin_speed)
if enemy.wound >= ENEMY_HP:
self.score += enemy.wound
enemy.die()
killist.append(i)
for i in reversed(killist): self.enemies.pop(i)
self.add_enemy()
def track_bullets(self):
"""Handle the bullets."""
fallen, time = [], get_ticks()
if (self.hero.firing and not self.hero.slashing
and time >= self.hero.next_strike):
self.hero.next_strike = time + ATTACK_SPEED
self.bullets.append(Bullet(self.surface, self.x, self.y,
self.hero.angle, 'Aluminium'))
self.bullets.extend(self.hero.shots)
fallen = []
block = (self.hero.spin_queue and self.hero.next_heal < 0
and self.hero.next_strike > self.hero.spin_queue / self.fps)
for i, bullet in enumerate(self.bullets):
wound = float(bullet.fall_time-time) / BULLET_LIFETIME
wound = bullet.fall_time / BULLET_LIFETIME
bullet.update(self.fps, self.distance)
if wound < 0:
gridx, gridy = self.get_grid(bullet.x, bullet.y)
if wound <= 0 or not self.isdisplayed(gridx, gridy):
fallen.append(i)
elif bullet.color == 'Aluminium':
x = MIDDLE + round2((bullet.x-self.x) / self.distance)
y = MIDDLE + round2((bullet.y-self.y) / self.distance)
if self.map[x][y] == WALL and time >= self.next_move:
active_enemies = [e for e in self.enemies if e.awake]
if self.map[gridx][gridy] == WALL and self.next_move <= 0:
fallen.append(i)
if not active_enemies: continue
self.glitch = wound * 1000
enemy = new_enemy(self, gridx, gridy)
enemy.awake = True
self.map[gridx][gridy] = ENEMY
play(SFX_SPAWN, enemy.x, enemy.y)
enemy.hit(wound)
self.enemies.append(enemy)
continue
for j, enemy in enumerate(self.enemies):
if not enemy.awake: continue
x, y = enemy.get_pos()
if bullet.get_distance(x, y) < self.distance:
for enemy in active_enemies:
if bullet.get_distance(*enemy.pos) < self.distance:
enemy.hit(wound)
if enemy.wound >= ENEMY_HP:
self.score += enemy.wound
enemy.die()
self.enemies.pop(j)
play(self.sfx_shot, wound, bullet.angle)
self.add_enemy()
play(bullet.sfx_hit, gridx, gridy, wound)
fallen.append(i)
break
elif bullet.get_distance(self.x, self.y) < self.distance:
if self.hero.spin_queue:
play(bullet.sfx_missed, wound, bullet.angle + pi)
if block:
self.hero.next_strike = (abs(self.hero.spin_queue/self.fps)
+ ATTACK_SPEED)
play(SFX_MISSED, gain=wound)
else:
self.hit_hero(wound, bullet.color)
play(bullet.sfx_hit, wound, bullet.angle + pi)
play(bullet.sfx_hit, gain=wound)
fallen.append(i)
for i in reversed(fallen): self.bullets.pop(i)
@ -288,81 +360,171 @@ class Maze:
return 0.0
for enemy in self.enemies:
x, y = self.get_pos(enemy.x, enemy.y)
if (max(abs(herox - x), abs(heroy - y)) * 2 < self.distance
and enemy.awake):
if max(abs(herox - x), abs(heroy - y)) * 2 < self.distance:
return 0.0
return vx or vy
def expos(self, x, y):
"""Return position of the given coordinates in rounded percent."""
cx = len(self.rangex)*50 + (x - self.centerx)/self.distance*100
cy = len(self.rangey)*50 + (y - self.centery)/self.distance*100
return round(cx), round(cy)
def update_export(self, forced=False):
"""Update the maze's data export and return the last record."""
if self.next_export > 0 and not forced or self.hero.dead: return
export = defaultdict(list)
export['s'] = self.get_score()
if self.next_move <= 0:
for y in self.rangey:
export['m'].append(''.join(
COLORS[self.get_color()] if self.map[x][y] == WALL else '0'
for x in self.rangex))
x, y = self.expos(self.x, self.y)
export['h'] = [
COLORS[self.hero.get_color()], x, y, deg(self.hero.angle),
int(self.hero.next_strike <= 0), int(self.hero.next_heal <= 0)]
for enemy in self.enemies:
if enemy.isunnoticeable(): continue
x, y = self.expos(*enemy.pos)
color, angle = COLORS[enemy.get_color()], deg(enemy.angle)
export['e'].append([color, x, y, angle])
for bullet in self.bullets:
x, y = self.expos(bullet.x, bullet.y)
color, angle = COLORS[bullet.get_color()], deg(bullet.angle)
if color != '0': export['b'].append([color, x, y, angle])
if self.next_export <= 0:
export['t'] = round(self.export_rate - self.next_export)
self.export.append(export)
self.next_export = self.export_rate
return export
def update(self, fps):
"""Update the maze."""
if self.paused: return
self.fps = fps
dx = self.is_valid_move(vx=self.vx)
self.centerx += dx
dy = self.is_valid_move(vy=self.vy)
self.centery += dy
self.vx = self.is_valid_move(vx=self.vx)
self.centerx += self.vx
self.vy = self.is_valid_move(vy=self.vy)
self.centery += self.vy
if dx or dy:
self.rotate()
self.next_move -= 1000 / fps
self.glitch -= 1000 / fps
self.next_slashfx -= 1000 / fps
self.next_export -= 1000 / fps
self.rotate()
if self.vx or self.vy or self.hero.firing or self.hero.slashing:
for enemy in self.enemies: enemy.wake()
for bullet in self.bullets: bullet.place(dx, dy)
for bullet in self.bullets: bullet.place(self.vx, self.vy)
self.draw()
for enemy in self.enemies: enemy.update()
self.track_bullets()
if not self.hero.dead:
self.hero.update(fps)
self.slash()
self.track_bullets()
pygame.display.flip()
pygame.display.set_caption('Brutal Maze - Score: {}'.format(
int(self.score - INIT_SCORE)))
if self.hero.wound >= HERO_HP: self.lose()
self.update_export()
def move(self, x, y, fps):
"""Command the hero to move faster in the given direction."""
stunned = get_ticks() < self.next_move
velocity = self.distance * HERO_SPEED / fps
accel = velocity * HERO_SPEED / fps
if stunned or not x:
self.vx -= sign(self.vx) * accel
if abs(self.vx) < accel * 2: self.vx = 0.0
elif x * self.vx < 0:
self.vx += x * 2 * accel
else:
self.vx += x * accel
if abs(self.vx) > velocity: self.vx = x * velocity
if stunned or not y:
self.vy -= sign(self.vy) * accel
if abs(self.vy) < accel * 2: self.vy = 0.0
elif y * self.vy < 0:
self.vy += y * 2 * accel
else:
self.vy += y * accel
if abs(self.vy) > velocity: self.vy = y * velocity
def resize(self, w, h):
def resize(self, size):
"""Resize the maze."""
size = self.w, self.h = w, h
self.surface = pygame.display.set_mode(size, RESIZABLE)
self.hero.resize()
self.w, self.h = size
self.surface = pygame.display.set_mode(size, pygame.RESIZABLE)
self.hero.resize(size)
offsetx = (self.centerx-self.x) / self.distance
offsety = (self.centery-self.y) / self.distance
self.distance = (w * h / 416) ** 0.5
self.x, self.y = w // 2, h // 2
self.distance = (self.w * self.h / 416) ** 0.5
self.x, self.y = self.w // 2, self.h // 2
self.centerx = self.x + offsetx*self.distance
self.centery = self.y + offsety*self.distance
w, h = int(w/self.distance/2 + 2), int(h/self.distance/2 + 2)
self.rangex = range(MIDDLE - w, MIDDLE + w + 1)
self.rangey = range(MIDDLE - h, MIDDLE + h + 1)
w, h = int(self.w/self.distance/2 + 1), int(self.h/self.distance/2 + 1)
self.rangex = list(range(MIDDLE - w, MIDDLE + w + 1))
self.rangey = list(range(MIDDLE - h, MIDDLE + h + 1))
self.slashd = self.hero.R + self.distance/SQRT2
def set_step(self, check=(lambda x, y: True)):
"""Work out next step on the shortest path to the destination.
Return whether target is impossible to reach and hero should
shoot toward it instead.
"""
if self.stepx or self.stepy and self.vx == self.vy == 0.0:
x, y = MIDDLE - self.stepx, MIDDLE - self.stepy
if self.stepx and not self.stepy:
nextx = x - self.stepx
n = self.map[x][y - 1] == EMPTY == self.map[nextx][y - 1]
s = self.map[x][y + 1] == EMPTY == self.map[nextx][y + 1]
self.stepy = n - s
elif not self.stepx and self.stepy:
nexty = y - self.stepy
w = self.map[x - 1][y] == EMPTY == self.map[x - 1][nexty]
e = self.map[x + 1][y] == EMPTY == self.map[x + 1][nexty]
self.stepx = w - e
return False
# Shoot WALL and ENEMY instead
if self.map[self.destx][self.desty] != EMPTY:
self.stepx = self.stepy = 0
return True
# Forest Fire algorithm
queue, visited = deque([(self.destx, self.desty)]), set()
while queue:
x, y = queue.pop()
if (x, y) in visited: continue
visited.add((x, y))
dx, dy = MIDDLE - x, MIDDLE - y
if dx**2 + dy**2 <= 2:
# Succeeded on finding a path
self.stepx, self.stepy = dx, dy
return False
for i, j in around(x, y):
if self.map[i][j] == EMPTY and check(i, j):
queue.appendleft((i, j))
# Failed to find way to move to target
self.stepx = self.stepy = 0
return True
def isfast(self):
"""Return if the hero is moving faster than HERO_SPEED."""
return (self.vx**2+self.vy**2)**0.5*self.fps > HERO_SPEED*self.distance
def dump_records(self):
"""Dump JSON records."""
if self.export_dir:
with open(json_rec(self.export_dir), 'w') as f:
json.dump(self.export, f, separators=JSON_SEPARATORS)
def lose(self):
"""Handle loses."""
self.hero.dead = True
self.hero.wound = HERO_HP
self.hero.slashing = self.hero.firing = False
self.destx = self.desty = MIDDLE
self.stepx = self.stepy = 0
self.vx = self.vy = 0.0
self.sfx_lose.play()
play(SFX_LOSE)
self.dump_records()
def reinit(self):
"""Open new game."""
self.centerx, self.centery = self.w / 2, self.h / 2
self.score, self.export = INIT_SCORE, []
self.new_map()
self.vx = self.vy = 0.0
self.rotatex = self.rotatey = 0
self.bullets, self.enemies = [], []
self.add_enemy()
self.next_move = self.next_slashfx = self.hero.next_strike = 0.0
self.target = LockOn(MIDDLE, MIDDLE, retired=True)
self.hero.next_heal = -1.0
self.hero.highness = 0.0
self.hero.slashing = self.hero.firing = self.hero.dead = False
self.hero.spin_queue = self.hero.wound = 0.0
self.hero.wounds = deque([0.0])

View File

@ -1,43 +1,34 @@
# -*- coding: utf-8 -*-
# characters.py - module for shared functions and macros
# This file is part of brutalmaze
# misc.py - module for miscellaneous functions
# Copyright (C) 2017-2020 Nguyễn Gia Phong
#
# brutalmaze 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 file is part of Brutal Maze.
#
# brutalmaze is distributed in the hope that it will be useful,
# Brutal Maze is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Brutal Maze 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.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with brutalmaze. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__doc__ = 'brutalmaze module for hero and enemy classes'
__doc__ = 'Brutal Maze module for miscellaneous functions'
from functools import reduce
from math import cos, sin, pi
from operator import or_
from random import uniform
from datetime import datetime
from itertools import chain
from math import cos, degrees, pi, sin
from os import path
from random import shuffle
import pygame
from pygame.gfxdraw import filled_polygon, aapolygon
from palace import Buffer, Source
from pygame.gfxdraw import aapolygon, filled_polygon
from .constants import MIDDLE
def some(a, keys):
"""Return True if there is a key k in keys that bool(a[k]) is True."""
return bool(reduce(or_, (a[k] for k in keys)))
def round2(number):
"""Round a number to an int."""
return int(round(number))
from .constants import ADJACENTS, CORNERS, MIDDLE
def randsign():
@ -46,9 +37,9 @@ def randsign():
def regpoly(n, R, r, x, y):
"""Return the pointlist of the regular polygon with n sides,
circumradius of R, the center point I(x, y) and one point A make the
vector IA with angle r (in radians).
"""Return pointlist of a regular n-gon with circumradius of R,
center point I(x, y) and corner A that angle of vector IA is r
(in radians).
"""
r %= pi * 2
angles = [r + pi*2*side/n for side in range(n)]
@ -56,7 +47,7 @@ def regpoly(n, R, r, x, y):
def fill_aapolygon(surface, points, color):
"""Draw a filled polygon with anti aliased edges onto a surface."""
"""Draw a filled polygon with anti-aliased edges onto a surface."""
aapolygon(surface, points, color)
filled_polygon(surface, points, color)
@ -66,37 +57,44 @@ def sign(n):
return -1 if n < 0 else 1 if n else 0
def cosin(x):
"""Return the sum of cosine and sine of x (measured in radians)."""
return cos(x) + sin(x)
def choices(d):
"""Choose a random key from a dict which has values being relative
weights of the coresponding keys.
def deg(x):
"""Convert angle x from radians to degrees,
casted to a nonnegative integer.
"""
population, weights = tuple(d.keys()), tuple(d.values())
cum_weights = [weights[0]]
for weight in weights[1:]: cum_weights.append(cum_weights[-1] + weight)
num = uniform(0, cum_weights[-1])
for i, w in enumerate(cum_weights):
if num <= w: return population[i]
return round((lambda a: a if a > 0 else a + 360)(degrees(x)))
def play(sound, volume, angle=None):
"""Play a pygame.mixer.Sound at the given volume."""
if pygame.mixer.find_channel() is None:
pygame.mixer.set_num_channels(pygame.mixer.get_num_channels() + 1)
if angle is None:
sound.set_volume(volume)
sound.play()
else:
delta = cos(angle)
volumes = [volume * (1-delta), volume * (1+delta)]
for i, v in enumerate(volumes):
if v > 1:
volumes[i - 1] += v - 1
volumes[i] = 1.0
sound.set_volume(1.0)
channel = sound.play()
channel.set_volume(*volumes)
def join(iterable, sep=' ', end='\n'):
"""Return a string which is the concatenation of string
representations of objects in the iterable, separated by sep.
end is appended to the resulting string.
"""
return sep.join(map(str, iterable)) + end
def around(x, y):
"""Return grids around the given one in random order."""
a = [(x + i, y + j) for i, j in ADJACENTS]
shuffle(a)
c = [(x + i, y + j) for i, j in CORNERS]
shuffle(c)
return chain(a, c)
def json_rec(directory):
"""Return path to JSON file to be created inside the given directory
based on current time local to timezone in ISO 8601 format.
"""
return path.join(
directory, '{}.json'.format(datetime.now().isoformat()[:19]))
def play(sound: str, x: float = MIDDLE, y: float = MIDDLE,
gain: float = 1.0) -> Source:
"""Play a sound at the given position."""
source = Buffer(sound).play()
source.spatialize = True
source.position = x, -y, 0
source.gain = gain
return source

44
brutalmaze/settings.ini Normal file
View File

@ -0,0 +1,44 @@
[Graphics]
Screen width: 640
Screen height: 480
# FPS should not be greater than refresh rate.
Maximum FPS: 60
[Sound]
Muted: no
# Volume must be between 0.0 and 1.0.
Music volume: 1.0
[Control]
# Touch-friendly control
Touch: no
# Input values should be either from Mouse1 to Mouse3 or a keyboard key
# and they are case-insensitively read.
# Aliases for special keys are listed here (without the K_ part):
# http://www.pygame.org/docs/ref/key.html
# Key combinations are not supported.
New game: F2
Toggle pause: p
Toggle mute: m
Move left: a
Move right: d
Move up: w
Move down: s
Long-range attack: Mouse1
Close-range attack: Mouse3
[Record]
# Directory to write record of game states, leave blank to disable.
Directory:
# Number of snapshots per second. This is preferably from 3 to 60.
Frequency: 30
[Server]
# Enabling remote control will disable control via keyboard and mouse.
Enable: no
Host: localhost
Port: 42069
# Timeout on blocking socket operations, in seconds.
Timeout: 1.0
# Disable graphics and sound (only if socket server is enabled).
Headless: no

View File

@ -1,31 +1,28 @@
# -*- coding: utf-8 -*-
# characters.py - module for weapon classes
# This file is part of brutalmaze
# Copyright (C) 2017-2020 Nguyễn Gia Phong
#
# brutalmaze 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 file is part of Brutal Maze.
#
# brutalmaze is distributed in the hope that it will be useful,
# Brutal Maze is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Brutal Maze 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.
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with brutalmaze. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong
# You should have received a copy of the GNU Affero General Public License
# along with Brutal Maze. If not, see <https://www.gnu.org/licenses/>.
__doc__ = 'brutalmaze module for weapon classes'
__doc__ = 'Brutal Maze module for weapon classes'
from math import cos, sin
from pygame.time import get_ticks
from pygame.mixer import Sound
from .constants import *
from .misc import regpoly, fill_aapolygon
from .constants import (BG_COLOR, BULLET_LIFETIME, BULLET_SPEED,
ENEMY_HP, SFX_SHOT_ENEMY, SFX_SHOT_HERO, TANGO)
from .misc import fill_aapolygon, regpoly
class Bullet:
@ -36,31 +33,37 @@ class Bullet:
x, y (int): coordinates of the center of the bullet (in pixels)
angle (float): angle of the direction the bullet pointing (in radians)
color (str): bullet's color name
fall_time (int): the tick that the bullet will fall down
sfx_hit (Sound): sound effect indicating the bullet hits the target
sfx_missed (Sound): sound effect indicating the bullet hits the target
fall_time (int): time until the bullet fall down
sfx_hit (str): sound effect indicating target was hit
"""
def __init__(self, surface, x, y, angle, color):
self.surface = surface
self.x, self.y, self.angle, self.color = x, y, angle, color
self.fall_time = get_ticks() + BULLET_LIFETIME
# Sound effects of bullets shot by hero are stored in Maze to avoid
# unnecessary duplication
if color != 'Aluminium':
self.sfx_hit = Sound(SFX_SHOT_HERO)
self.sfx_missed = Sound(SFX_MISSED)
self.fall_time = BULLET_LIFETIME
if color == 'Aluminium':
self.sfx_hit = SFX_SHOT_ENEMY
else:
self.sfx_hit = SFX_SHOT_HERO
def update(self, fps, distance):
"""Update the bullet."""
s = distance * BULLET_SPEED / fps
self.x += s * cos(self.angle)
self.y += s * sin(self.angle)
pentagon = regpoly(5, distance // 4, self.angle, self.x, self.y)
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
self.fall_time -= 1000 / fps
def get_color(self):
"""Return current color of the enemy."""
value = int((1 - self.fall_time/BULLET_LIFETIME) * ENEMY_HP)
try:
fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])
return TANGO[self.color][value]
except IndexError:
pass
return BG_COLOR
def draw(self, radius):
"""Draw the bullet."""
pentagon = regpoly(5, radius, self.angle, self.x, self.y)
fill_aapolygon(self.surface, pentagon, self.get_color())
def place(self, x, y):
"""Move the bullet by (x, y) (in pixels)."""
@ -70,3 +73,22 @@ class Bullet:
def get_distance(self, x, y):
"""Return the from the center of the bullet to the point (x, y)."""
return ((self.x-x)**2 + (self.y-y)**2)**0.5
class LockOn:
"""Lock-on device to assist hero's aiming.
This is used as a mutable object to represent a grid of wall.
Attributes:
x, y (int): coordinates of the target (in grids)
retired (bool): flag indicating if the target is retired
"""
def __init__(self, x, y, retired=False):
self.x, self.y = x, y
self.retired = retired
def place(self, x, y, isdisplayed):
"""Move the target by (x, y) (in grids)."""
self.x += x
self.y += y
if not isdisplayed(self.x, self.y): self.retired = True

View File

@ -0,0 +1,104 @@
using System;
using System.Text;
using System.Net.Sockets;
namespace BrutalmazeClient
{
class Program
{
static void Main(string[] args)
{
const string host = "localhost";
const int port = 42069;
Socket client_socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
client_socket.Connect(host, port);
Random rnd = new Random();
int recv, sent;
byte[] buff = new byte[1000];
byte[] query;
string[] matrix = new string[100];
const int MAGIC = 42; // For escape
string l, data, l1;
int sz, nl;
int nh, ne, nb, score;
char hC;
int hX, hY, hA, canAtk, canReg;
int prevX = 1234, prevY = 5678;
int dir = 0, deg = 0, atk = 1;
int needEsc = 0;
while (42 < 420)
{
try
{
recv = client_socket.Receive(buff, 7, 0);
}
catch (SocketException e)
{
Console.WriteLine(e.ToString());
break;
}
l = Encoding.ASCII.GetString(buff, 0, 7);
sz = Int32.Parse(l);
if (sz == 0)
break;
recv = client_socket.Receive(buff, sz, 0);
data = Encoding.ASCII.GetString(buff, 0, sz);
// Standardize Data
nl = 0;
l1 = data.Split('\n')[nl];
nh = Int32.Parse(l1.Split(' ')[0]);
ne = Int32.Parse(l1.Split(' ')[1]);
nb = Int32.Parse(l1.Split(' ')[2]);
score = Int32.Parse(l1.Split(' ')[3]);
for (int i = 0; i < nh; ++i, ++nl)
matrix[i] = data.Split('\n')[i + 1];
l1 = data.Split('\n')[++nl];
hC = Char.Parse(l1.Split(' ')[0]);
hX = Int32.Parse(l1.Split(' ')[1]);
hY = Int32.Parse(l1.Split(' ')[2]);
hA = Int32.Parse(l1.Split(' ')[3]);
canAtk = Int32.Parse(l1.Split(' ')[4]);
canReg = Int32.Parse(l1.Split(' ')[5]);
for(int i = 1; i <= ne; ++i, ++nl)
{
}
for(int i = 1; i <= nb; ++i, ++nl)
{
}
// Process
if (needEsc == 0)
{
dir = 0;
if (prevX == hX && prevY == hY)
{
int matX = hX / 100, matY = hY / 100;
if (matrix[matY - 1][matX + 2] == '0' && matrix[matY - 1][matX - 2] == '1')
{
dir = 5;
needEsc = 1;
}
if (matrix[matY - 1][matX + 2] == '1' && matrix[matY + 1][matX - 2] == '0')
{
dir = 7;
needEsc = 1;
}
}
}
else
{
needEsc = (needEsc + 1) % MAGIC;
}
deg = rnd.Next(-4, 5) * 10;
atk = rnd.Next(1, 1);
query = Encoding.ASCII.GetBytes(dir.ToString() + " " + deg.ToString() + " " + atk.ToString());
sent = client_socket.Send(query);
prevX = hX;
prevY = hY;
}
client_socket.Shutdown(SocketShutdown.Both);
client_socket.Close();
}
}
}

72
client-examples/hit-and-run.py Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
from contextlib import closing, suppress
from math import atan2, degrees, inf
from random import randrange, shuffle
from socket import socket
AROUND = [5, 2, 1, 0, 3, 6, 7, 8]
def get_moves(y, x):
"""Return tuple of encoded moves."""
return ((y - 1, x - 1), (y - 1, x), (y - 1, x + 1), # noqa
(y, x - 1), (y, x), (y, x + 1), # noqa
(y + 1, x - 1), (y + 1, x), (y + 1, x + 1)) # noqa
def is_wall(maze, y, x):
"""Return weather the cell (x, y) is wall."""
return maze[y][x] != '0'
def get_move(maze, move):
"""Return an outstanding move."""
moves, around = get_moves(len(maze) // 2, len(maze[0]) // 2), AROUND[:]
if move != 4 and not is_wall(maze, *moves[move]): return move
if move == 4:
shuffle(around)
else:
idx = AROUND.index(move)
around.sort(key=lambda i: abs(abs(abs(AROUND.index(i)-idx)-4)-4))
for move in around:
idx = AROUND.index(move)
if all(not is_wall(maze, *moves[i])
for i in (move, AROUND[idx - 1], AROUND[idx - 7])):
return move
return 4
with suppress(KeyboardInterrupt), closing(socket()) as sock:
sock.connect(('localhost', 42069))
move = 4
while True:
length = sock.recv(7).decode()
# connection closed or game over
if length in ('', '0000000'): break
data = iter(sock.recv(int(length)).decode().split())
nh, ne, nb, score = (int(next(data)) for i in range(4))
maze = [list(next(data)) for i in range(nh)]
hp = (lambda c: 0 if c == 48 else 123 - c)(ord(next(data)))
hx, hy, ha = (int(next(data)) for i in range(3))
attackable, heal = (bool(int(next(data))) for i in range(2))
if nh: move = get_move(maze, move)
angle, shortest = ha, inf
for i in range(ne):
p = 3 - (ord(next(data)) - 97)%3
x, y, a = (int(next(data)) for j in range(3))
d = ((x - hx)**2 + (y - hy)**2)**0.5
if d < shortest:
shortest = d
b = degrees(atan2(y - hy, x - hx))
angle = round(b + 360 if b < 0 else b)
if hp <= 2 and heal:
move, attack = 4, 2
elif not ne:
attack = randrange(3) * (attackable and hp > 2)
elif shortest < 160:
move, angle, attack = AROUND[round(angle/45 - 0.5) - 4], ha, 2
else:
attack = 1
sock.send(f'{move} {angle} {attack}'.encode())

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
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)

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

1
docs/requirements.txt Normal file
View File

@ -0,0 +1 @@
sphinx >= 3.*

View File

@ -0,0 +1,101 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
// Copyright (C) 2018 Nguyễn Gia Phong
const PERIGON = Math.PI * 2;
const TANGO = {'a': '#fce94f', 'b': '#edd400', 'c': '#c4a000', // Butter
'd': '#fcaf3e', 'e': '#f57900', 'f': '#ce5c00', // Orange
'g': '#e9b96e', 'h': '#c17d11', 'i': '#8f5902', // Chocolate
'j': '#8ae234', 'k': '#73d216', 'l': '#4e9a06', // Chameleon
'm': '#729fcf', 'n': '#3465a4', 'o': '#204a87', // Sky Blue
'p': '#ad7fa8', 'q': '#75507b', 'r': '#5c3566', // Plum
's': '#ef2929', 't': '#cc0000', 'u': '#a40000', // Scarlet Red
'v': '#eeeeec', 'w': '#d3d7cf', 'x': '#babdb6', // Aluminium
'y': '#888a85', 'z': '#555753', '0': '#2e3436'};
var mw, mh; // maze width and height in grids
// Resize canvas to fit page.
function resizeCanvas(canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// Draw on the given canvas a c-colored regular n-gon with circumradius of R,
// center point I(x, y) and corner A that angle of vector IA is r (in radians).
function drawPolygon(canvas, n, c, x, y, r, R) {
var ctx = canvas.getContext('2d');
ctx.beginPath();
r = r * Math.PI / 180 % PERIGON;
ctx.moveTo(x + R*Math.cos(r), y + R*Math.sin(r));
for (var i = 1; i < n; i++) {
r += PERIGON / n;
ctx.lineTo(x + R*Math.cos(r), y + R*Math.sin(r));
}
ctx.closePath();
ctx.fillStyle = TANGO[c];
ctx.fill();
}
// Draw the maze, hero, enemies and bullets of the given frame.
function drawFrame(canvas, frame) {
var cw = canvas.width, ch = canvas.height;
var maze = frame.m;
if (maze) {
mw = maze[0].length;
mh = maze.length;
}
unit = Math.min(cw / (mw + 1), ch / (mh + 1));
eR = unit / Math.sqrt(2);
hR = unit * 2 / Math.pow(27, 0.25);
bR = unit / 4;
var hero = frame.h;
var x0 = cw/2 - hero[1]/100*unit, y0 = ch/2 - hero[2]/100*unit;
canvas.getContext('2d').clearRect(0, 0, cw, ch);
if (maze)
for (var row = 0; row < mh; row++)
for (var column = 0; column < mw; column++)
if (maze[row][column] != '0') {
var x = x0 + column*unit, y = y0 + row*unit;
var ctx = canvas.getContext('2d');
ctx.fillStyle = TANGO[maze[row][column]];
ctx.fillRect(x, y, unit + 1, unit + 1);
}
if (frame.e)
for (let enemy of frame.e)
drawPolygon(canvas, 4, enemy[0], x0 + enemy[1]/100*unit,
y0 + enemy[2]/100*unit, enemy[3], eR);
drawPolygon(canvas, 4 - hero[5], hero[0], cw / 2, ch / 2, hero[3], hR);
if (frame.b)
for (let bullet of frame.b)
drawPolygon(canvas, 5, bullet[0], x0 + bullet[1]/100*unit,
y0 + bullet[2]/100*unit, bullet[3], bR);
}
// Recursive function to loop with window.setTimeout.
function playRecord(canvas, record, index) {
if (index >= record.length) {
document.title = 'Brutal Maze record player';
document.getElementById('input').style.display = '';
return;
}
frame = record[index];
document.title = `Score: ${frame.s}`;
setTimeout(function () {
drawFrame(canvas, frame);
playRecord(canvas, record, index + 1);
}, frame.t);
}
// Fetch JSON record and parse to playRecord.
function playJSON() {
fetch(document.getElementById('record').value).then(function(res) {
return res.json();
}).then(function(record) {
document.getElementById('input').style.display = 'none';
playRecord(document.getElementById('canvas'), record, 0);
}).catch(error => alert(error));
}
// @license-end

View File

@ -0,0 +1,34 @@
html, body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
display: block;
background-color: #2e3436;
}
canvas {
left: 0px;
top: 0px;
background-color: #2e3436;
}
div {
display: flex;
justify-content: center;
}
input {
border: none;
border-radius: 4px;
background-color: #eeeeec;
color: #2e3436;
font-size: 1.25em;
padding: 0.3em;
margin: 0.2em 0.1em;
}
input[type=text] {
width: 50vw;
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<meta charset='utf-8'>
<title>Brutal Maze record player</title>
<link rel='icon' type='image/png' href='_static/favicon.ico'>
<link rel='stylesheet' type='text/css' href='_static/recplayer.css'>
<script src='_static/brutalma.js'></script>
<body>
<div id='input'>
<input id='record' type='text' name='record' value='record.json'>
<input id='button' type='button' value='Play JSON record'>
</div>
<canvas id='canvas' width='640' height='480'></canvas>
<script>
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
var canvas = document.getElementById('canvas');
resizeCanvas(canvas);
window.onresize = function() {resizeCanvas(canvas)};
document.getElementById('record').onkeypress = function (event) {
if (event.key == 'Enter') {
playJSON();
}
};
document.getElementById('button').onclick = playJSON;
// @license-end
</script>
</body>
</html>

61
docs/source/conf.py Normal file
View File

@ -0,0 +1,61 @@
# 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 = 'Brutal Maze'
copyright = '2017-2020, Nguyễn Gia Phong' # noqa
author = 'Nguyễn Gia Phong'
# The full version, including alpha/beta/rc tags
release = '0.9.4'
# -- 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 = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# 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 = []
# -- 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 = 'alabaster'
html_theme_options = {'fixed_sidebar': True, 'show_relbars': True}
html_logo = 'icon.svg'
html_favicon = 'favicon.ico'
html_extra_path = ['record.json']
html_additional_pages = {'recplayer': 'recplayer.html'}
# 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']

129
docs/source/config.rst Normal file
View File

@ -0,0 +1,129 @@
Configuration
=============
Configuration Files
-------------------
At the time of writing, this is the default configuration file:
.. code-block:: ini
[Graphics]
Screen width: 640
Screen height: 480
# FPS should not be greater than refresh rate.
Maximum FPS: 60
[Sound]
Muted: no
# Volume must be between 0.0 and 1.0.
Music volume: 1.0
# Use space music background, which sounds cold and creepy.
Space theme: no
[Control]
# Touch-friendly control
Touch: no
# Input values should be either from Mouse1 to Mouse3 or a keyboard key
# and they are case-insensitively read.
# Aliases for special keys are listed here (without the K_ part):
# http://www.pygame.org/docs/ref/key.html
# Key combinations are not supported.
New game: F2
Toggle pause: p
Toggle mute: m
Move left: a
Move right: d
Move up: w
Move down: s
Long-range attack: Mouse1
Close-range attack: Mouse3
[Record]
# Directory to write record of game states, leave blank to disable.
Directory:
# Number of snapshots per second. This is preferably from 3 to 60.
Frequency: 30
[Server]
# Enabling remote control will disable control via keyboard and mouse.
Enable: no
Host: localhost
Port: 42069
# Timeout on blocking socket operations, in seconds.
Timeout: 1.0
# Disable graphics and sound (only if socket server is enabled).
Headless: no
By default, Brutal Maze also then tries to read site (system-wide)
and user configuration.
Site Config File Location
^^^^^^^^^^^^^^^^^^^^^^^^^
* Apple macOS: ``/Library/Application Support/brutalmaze/settings.ini``
* Other Unix-like: ``$XDG_CONFIG_DIRS/brutalmaze/settings.ini`` or
``/etc/xdg/brutalmaze/settings.ini``
* Microsoft Windows:
* XP: ``C:\Documents and Settings\All Users\Application Data\brutalmaze\settings.ini``
* Vista: Fail! (``C:\ProgramData`` is a hidden *system* directory,
however if you use Windows Vista, please file an issue telling us
which error you receive)
* 7 and above: ``C:\ProgramData\brutalmaze\settings.ini``
User Config File Location
^^^^^^^^^^^^^^^^^^^^^^^^^
* Apple macOS: ``~/Library/Application Support/brutalmaze/settings.ini``
* Other Unix-like: ``$XDG_CONFIG_HOME/brutalmaze/settings.ini`` or
``~/.config/brutalmaze/settings.ini``
* Microsoft Windows (roaming is not supported until someone requests):
* XP: ``C:\Documents and Settings\<username>\Application Data\brutalmaze\settings.ini``
* Vista and above: ``C:\Users\<username>\AppData\Local\brutalmaze\settings.ini``
Command-Line Arguments
----------------------
.. code-block:: console
$ brutalmaze --help
usage: brutalmaze [options]
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
--write-config [PATH]
write default config and exit, if PATH not specified use stdout
-c PATH, --config PATH
location of the configuration file
-s X Y, --size X Y the desired screen size
-f FPS, --max-fps FPS
the desired maximum FPS
--mute, -m mute all sounds
--unmute unmute sound
--music-volume VOL between 0.0 and 1.0
--space-music use space music background
--default-music use default music background
--touch enable touch-friendly control
--no-touch disable touch-friendly control
--record-dir DIR directory to write game records
--record-rate SPF snapshots of game state per second
--server enable server
--no-server disable server
--host HOST host to bind server to
--port PORT port for server to listen on
-t TIMEOUT, --timeout TIMEOUT
socket operations timeout in seconds
--head run server with graphics and sound
--headless run server without graphics or sound
First, Brutal Mazes read the default settings, then it try to read site and
user config whose locations are shown above. These files are listed as fallback
of the ``--config`` option and their contents are fallback for other options
(if they are absent default values are used instead). We don't support control
configuration via CLI because that is unarguably ugly.
If ``--config`` option is set, Brutal Maze parse it before other command-line
options. Later-read preferences will override previous ones.

109
docs/source/copying.rst Normal file
View File

@ -0,0 +1,109 @@
Copying
=======
This listing is our best-faith, hard-work effort at accurate attribution,
sources, and licenses for everything in Brutal Maze. If you discover
an asset/contribution that is incorrectly attributed or licensed,
please contact us immediately. We are happy to do everything we can
to fix or remove the issue.
License
-------
Brutal Maze's source code and its icon are released under GNU Affero General
Public License version 3 or later. This means if you run a modified program on
a server and let other users communicate with it there, your server must also
allow them to download the source code corresponding to the modified version
running there.
.. image:: https://www.gnu.org/graphics/agplv3-155x51.png
:target: https://www.gnu.org/licenses/agpl.html
Other creative works retain their original licenses as listed below.
Color Palette
-------------
Brutal Maze uses the Tango color palette by `the Tango desktop project`_
to draw all of its graphics. The palette is released to the Public Domain.
Sound Effects
-------------
Sound Effects Artist---Tobiasz 'unfa_' Karoń
* License: `CC BY 3.0`_
* brutalmaze/soundfx/heart.ogg (original__)
__ https://freesound.org/s/217456
Sound Effects Artist---HappyParakeet_
* License: `CC0 1.0`_
* brutalmaze/soundfx/lose.ogg (original__)
__ https://freesound.org/s/398068
Sound Effects Artist---jameswrowles_
* License: `CC0 1.0`_
* brutalmaze/soundfx/missed.ogg (original__)
__ https://freesound.org/s/380641
Sound Effects Artist---MrPork_
* License: `CC0 1.0`_
* brutalmaze/soundfx/noise.ogg (original__)
__ https://freesound.org/s/257449
Sound Effects Artist---suspensiondigital_
* License: `CC0 1.0`_
* brutalmaze/soundfx/shot-enemy.ogg (original__)
__ https://freesound.org/s/389704
Sound Effects Artist---gusgus26_
* License: `CC0 1.0`_
* brutalmaze/soundfx/shot-hero.ogg (original__)
__ https://freesound.org/s/121188
Sound Effects Artist---braqoon_
* License: `CC0 1.0`_
* brutalmaze/soundfx/slash-enemy.ogg (original__)
__ https://freesound.org/s/161098
Sound Effects Artist---Qat_
* License: `CC0 1.0`_
* brutalmaze/soundfx/slash-hero.ogg (original__)
__ https://freesound.org/s/108333
Sound Effects Artist---pepingrillin_
* License: `CC0 1.0`_
* brutalmaze/soundfx/spawn.ogg (original__)
__ https://freesound.org/s/252083
.. _CC BY 3.0: https://creativecommons.org/licenses/by/3.0/legalcode
.. _CC0 1.0: https://creativecommons.org/publicdomain/zero/1.0/legalcode
.. _CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/legalcode
.. _the Tango desktop project: http://tango-project.org/
.. _unfa: https://freesound.org/people/unfa/
.. _HappyParakeet: https://freesound.org/people/HappyParakeet/
.. _jameswrowles: https://freesound.org/people/jameswrowles/
.. _MrPork: https://freesound.org/people/MrPork/
.. _suspensiondigital: https://freesound.org/people/suspensiondigital/
.. _gusgus26: https://freesound.org/people/gusgus26/
.. _braqoon: https://freesound.org/people/braqoon/
.. _Qat: https://freesound.org/people/Qat/
.. _pepingrillin: https://freesound.org/people/pepingrillin/

BIN
docs/source/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

116
docs/source/gameplay.rst Normal file
View File

@ -0,0 +1,116 @@
Gameplay
========
Brutal Maze is a fast-paced hack and slash game which aims to bring players
a frustrating, horror-like experience. It tries to mimic real-life logic in
a way that truly represents our loneliness, mortality and helplessness
in the universe.
The game features a solitary hero in a trigon shape who got lost in world of
squares. Unlucky for per, the squares have no intention to let their visitor
leave in peace. Together, they form a greater being called The Maze.
Naturally, The Maze is dynamically and infinitely generated. As our poor hero
tries to find a way out, it releases its minions (we will call them *enemies*
from here) to stop per. Since The Maze *sees it all* and *knows it all*,
it keeps creating more and more enemies for the hero to fight. It also
keeps track of which type of squares can do most damages to our trigon,
in order to send out the most effective belligerents.
Your mission is to help the hero go the furthest distance possible,
while fighting those aggressive enemies. Extra information below will
give you a better understanding of what you fight and how you fight them.
Hero
----
The hero is a regular trigon_ in Aluminium color (from `the Tango palette`_).
Perse has the ability to attack and move (both horizontally and vertically)
simultaneously. However, close- and long-range attacks can't be stricken
at the same time. When swinging per blade, our hero may also avoid getting
damages caused by per enemies' bullets.
Like heroes in other hack and slash games, the trigon can heal too, but irony,
per HP recovery rate decreases as perse gets wounded. Been warned you have,
bravery will only give you regrets.
Enemies
-------
Enemies are put into hibernation and blend into The Maze at the time of their
creation. When the hero comes across, they become awake and show their
(physical) colors. Enemy of each color has an unique power as described below:
Butter
May strike critical hits.
Orange (also known as *Agent Orange*)
May prevent the hero from healing or blocking bullets.
Poisoned hero will be drawn as a square.
Chocolate (a.k.a. *MDMA in Disguise*):
May make the hero high and shoot uncontrollably.
Still, Chocolate is good for your health (and so is MDMA).
Chameleon
Invisible, only shows itself when attacking or being attacked.
Sky Blue (a.k.a. *Lightning Sky*)
May immobilize the hero. If this happen our hero can only see the enemies,
including Chameleons, on a blank background. What a blessing in disguise!
Plum (a.k.a. *Plum Wine*)
May replicate. Very quickly.
Scarlet Red (a.k.a. *Vampire's Eye*)
Moves faster and drains hero's HP.
The possibility that an enemy attack with its special power increases when it's
able to prove its effectiveness to The Maze. In other words, the more a kind
of enemy hit the hero, the more chance they *may* use their unique abilities.
Lucky for you, squares are unitasking so they have to stop moving to perform
attacks. This slows them down a bit, however the ones which fall off the
display will respawn elsewhere in The Maze.
Attacks
-------
In this game, attack's damage is contingent on the distance between the
attacker and its target. The closer they are, the more damage is caused.
There is at least an one-third-second delay between two attacks stricken
by any character.
Long-range Attacks
^^^^^^^^^^^^^^^^^^
While projectiles are often called *bullets* in the code and the documentation,
they are more similar to stones propelled by slingshots, as they don't fly very
far (about 6 times the width of an enemy). Those fired by enemies can fly
though walls but the ones shot by the hero turn the grid into a new enemy.
A bullet is counted as hitting the target when the distance between the center
of the two object is less than the circumradius of a cell.
Close-range Attacks
^^^^^^^^^^^^^^^^^^^
It is needless to explain any further on how this kind of attack works, so we
only provide the size of the characters for you to calculate when the strike
can wound the target. To do so, the attacker must *touch* its opponents, or
simplistically, the distance between the central points of the two characters
must not be any greater than the sum of their circumradiuses. Do the
calculations yourself, a square's side is a fifth of the walls', and covers the
same area as a trigon.
Specially, hero's closed-range attacks also block opponents' bullets.
If this happens, the hero won't be able to attack in the next turn.
Manual slashing
^^^^^^^^^^^^^^^
As the hero always follow the mouse, perse perform close-range attack
while doing so. Unlike the automatic ones, there isn't any delay between
two manual slashings.
.. _trigon:
https://www.pygame.org/docs/ref/gfxdraw.html#pygame.gfxdraw.aatrigon
.. _the Tango palette:
https://en.wikipedia.org/wiki/Tango_Desktop_Project#Palette

24
docs/source/icon.svg Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" standalone="no"?>
<!--
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
International License. To view a copy of this license, visit
http://creativecommons.org/licenses/by-sa/4.0/ or send a letter
to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="576" height="576" viewBox="-288 -288 576 576">
<title>Brutal Maze icon in Scalable Vector Graphics</title>
<path fill="#2e3436"
d="M 0 288
C 126.2 288, 196.3563 288, 242.1782 242.1782
C 288 196.3563, 288 126.2, 288 0
C 288 -126.2, 288 -196.3563, 242.1782 -242.1782
C 196.3563 -288, 126.2,-288, 0 -288
C -126.2 -288, -196.3563 -288, -242.1782 -242.1782
C -288 -196.3563, -288 -126.2, -288 0
C -288 126.2, -288 196.3563, -242.1782 242.1782
C -196.3563 288, -126.2 288, 0 288
Z"/>
<polygon points="-119.1174 -119.1174, 162.7174 -43.6, -43.6 162.7174"
style="fill:#eeeeec"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

37
docs/source/index.rst Normal file
View File

@ -0,0 +1,37 @@
Overview
========
Brutal Maze is a thrilling shoot 'em up game with minimalist art style.
.. image:: images/screenshot.png
Notable features:
* Being highly portable.
* Auto-generated and infinite maze.
* No binary data for drawing.
* Enemies with special abilities: stun, poison, camo, etc.
* Somewhat a realistic physic and logic system.
* Resizable game window in-game.
* Easily customizable via INI file format.
* Recordable in JSON (some kind of silent screencast).
* Remote control through TCP/IP socket (can be used in AI researching).
Table of Contents
-----------------
.. toctree::
:maxdepth: 2
install
config
gameplay
remote
copying
Record Player
-------------
.. raw:: html
<iframe src='recplayer.html' width=640 height=480></iframe>

38
docs/source/install.rst Normal file
View File

@ -0,0 +1,38 @@
Installation
============
Brutal Maze should run on Python version 3.6 and above.
If you're using an Unix-like operating system, you're likely
to have Python installed on your computer. Otherwise, you can
download it from python.org_.
The game also uses multiple third-party libraries, which is recommended to
be installed using ``pip``. There is a detailed documentation about getting
this package manager on pypa.io_.
Install from PyPI
-----------------
For convenience reasons, every release of Brutal Maze is uploaded to the Python
Package Index. To either install or upgrade, open your terminal (on Windows:
Command Prompt or PowerShell) and run::
pip install --user --upgrade brutalmaze
This requires the `the user scheme`_ scripts directory to be
in your environmental variable ``$PATH``.
Install from Source
-------------------
If you want to tweak the game or contribute, clone the git repository::
git clone https://git.disroot.org/McSinyx/brutalmaze.git
Then install it using ``pip``, like so::
pip install --user brutalmaze/
.. _python.org: https://www.python.org/downloads/
.. _pypa.io: https://pip.pypa.io/en/latest/installing/
.. _the user scheme: https://docs.python.org/3/install/index.html#alternate-installation-the-user-scheme

1
docs/source/record.json Normal file

File diff suppressed because one or more lines are too long

250
docs/source/remote.rst Normal file
View File

@ -0,0 +1,250 @@
Remote Control
==============
Brutal Maze provides a INET (i.e. IPv4), STREAM (i.e. TCP) socket server which
can be enabled in the config file or by adding the ``--server`` CLI flag.
After binding to the given host and port, it will wait for a client to connect.
Then, in each cycle of the loop, the server will send current details of
each object (hero, walls, enemies and bullets), wait for the client to process
the data and return instruction for the hero to follow. Since there is no EOT
(End of Transfer) on a socket, messages sent and received between server
and client must must be strictly formatted as explained below.
Server Output
-------------
First, the game will export its data to a byte sequence (which in this case,
is simply a ASCII string without null-termination) of the length :math:`l`.
Before sending the data to the client, the server would send the number
:math:`l` padded to 7 digits.
Below is the meta structure of the data::
<Map height (nh)> <Number of enemies (ne)> <Number of bullets (nb)> <Score>
<nh lines describing visible part of the maze>
<One line describing the hero>
<ne lines describing ne enemies>
<nb lines describing nb bullets>
The Maze
^^^^^^^^
Visible parts of the maze with the width :math:`n_w` and the height :math:`n_h`
are exported as a byte map of :math:`n_h` lines and :math:`n_w` columns.
Any character other than 0 represents a blocking *cell*, i.e. a wall.
To avoid floating point number in later description of other objects, each
cell has the width (and height) of 100, which means the top left corner of
the top left cell has the coordinates of :math:`(0, 0)` and the bottom right
vertex of the bottom right cell has the coordinates of
:math:`(100 n_w, 100 n_h)`.
The Hero
^^^^^^^^
6 properties of the hero are exported in one line,
separated by 1 space, in the following order:
:Color:
The current HP of the hero, as shown in in the later section.
:X-coordinate:
An integer within :math:`[0, 100 n_w]`.
:Y-coordinate:
An integer within :math:`[0, 100 n_h]`.
Note that the y-axis points up-side-down instead of pointing upward.
:Angle:
The direction the hero is pointing to in degrees,
cast to an integer from 0 to 360. Same note as above
(the unit circle figure might help you understand this easier).
:Can attack:
0 for *no* and 1 for *yes*.
:Can heal:
0 for *no* and 1 for *yes*.
.. image:: images/unit-circle.png
The Enemies
^^^^^^^^^^^
Each enemy exports these properties:
:Color:
The type and the current HP of the enemy, as shown in the table below.
:X-coordinate:
An integer within :math:`[0, 100 n_w]`.
:Y-coordinate:
An integer within :math:`[0, 100 n_h]`.
:Angle:
The direction the enemy is pointing to in degrees,
cast to a nonnegative integer.
To shorten the data, each color (in the Tango palette) is encoded to a
lowercase letter. Different shades of a same color indicating different HP
of the characters.
=========== ======== ======== ======== ======== ========
HP 5 4 3 2 1
=========== ======== ======== ======== ======== ========
Butter |fce94f| |edd400| |c4a000|
Orange |fcaf3e| |f57900| |ce5c00|
Chocolate |e9b96e| |c17d11| |8f5902|
Chameleon |8ae234| |73d216| |4e9a06|
Sky Blue |729fcf| |3465a4| |204a87|
Plum |ad7f8a| |75507b| |5c3566|
Scarlet Red |ef2929| |cc0000| |a40000|
Aluminium |eeeeec| |d3d7cf| |babdb6| |888a85| |555753|
=========== ======== ======== ======== ======== ========
.. note::
If a character shows up with color ``0``, it is safe to ignore it
since it is a dead body yet to be cleaned up.
Flying bullets
^^^^^^^^^^^^^^
Bullets also export 4 properties like enemies:
:Color:
The type and potential damage of the bullet (from 0.0 to 1.0),
encoded similarly to characters', except that aluminium bullets
only have 4 colors ``v``, ``w``, ``x`` and ``0``.
:X-coordinate:
An integer within :math:`[0, 100 n_w]`.
:Y-coordinate:
An integer within :math:`[0, 100 n_h]`.
:Angle:
The bullet's flying direction in degrees,
cast to a nonnegative integer.
Example
^^^^^^^
.. image:: images/screenshot.png
Above snapshot of the game is exported as:
.. code-block:: text
19 5 3 180
00000000000000000vvvv0000
v0000000000000000vvvv0000
v0000000000000000vvvv0000
v0000000000000000vvvv0000
vvvvvvvvvvvvvvvvvvvvv0000
vvvvvvvvvvvvvvvvvvvvv000v
vvvvvvvvvvvvvvvvvvvvv000v
vvvvvvvvvvvvvvvvvvvv00000
0000000000000000000000000
0000000000000000000000000
0000000000000000000000000
v000000000000000000000000
v000000000000000000000000
v000000000000000000000000
v000vvvvvvv000vvv0vvv0000
v000vvvvvvv000vvvvvvv0000
v000vvvvvvv000vvvvvvv0000
v000vvvvvvv000vvvvvvv0000
v000000vvvv000000vvvv0000
v 1267 975 47 0 1
p 1817 1050 45
g 1550 1217 45
a 2250 1194 45
p 2050 1017 45
e 1850 950 358
x 2126 1189 361
e 1541 1020 167
v 1356 1075 49
Client Output Format
--------------------
Every loop, the server receives no more than 7 bytes in the format of
``<Movement> <Angle> <Attack>``. Again, these values need to be
specially encoded.
Movement
^^^^^^^^
This is the most awkward one. As we can all imagine, there are nine different
directions for the hero to move. Were they represented as two-dimensional
vectors, at least three characters would be needed to describe such
a simple thing, e.g. ``1 0`` for :math:`m = (1, 0)`, and in the worst-case
scenario :math:`m = (-1, -1)`, we would need five: ``-1 -1``. 40 bits are used
to carry a four-bit piece of data, freaking insane, right? So instead,
we decided to *slightly* encode it like this:
========= ==== === =====
Direction Left Nil Right
========= ==== === =====
**Up** 0 1 2
**Nil** 3 4 5
**Down** 6 7 8
========= ==== === =====
Angle
^^^^^
Direction to point to hero to, might be useful to aim or to perform
a close-range attack manually. This value should also be converted
to degrees and casted to a nonnegative integer.
Attack
^^^^^^
Attack can be either of the three values:
0. Do nothing
1. Long-range attack
2. Close-range attack
Simple, huh? Though be aware that this won't have any effect if the hero
can yet strike an attack (as described in above section about `The Hero`_).
Pseudo-Client
-------------
#. Create an INET, STREAMing socket ``sock``
#. Connect ``sock`` to the address ``host:port`` which the server is bound to
#. Receive length :math:`l` of data
#. If :math:`l > 0`, close ``sock`` and quit
#. Receive the data
#. Process the data
#. Send instruction for the hero to the server and go back to step 3
Your AI should try to not only reach the highest score possible, but also in
the smallest amount of time. For convenience purpose, the server will
log these values to stdout.
There are samples of client implementations in different languages in
the client-examples_ directory (more are coming).
.. _client-examples:
https://git.disroot.org/McSinyx/brutalmaze/src/branch/master/client-examples
.. |204a87| image:: images/204a87.png
.. |3465a4| image:: images/3465a4.png
.. |4e9a06| image:: images/4e9a06.png
.. |555753| image:: images/555753.png
.. |5c3566| image:: images/5c3566.png
.. |729fcf| image:: images/729fcf.png
.. |73d216| image:: images/73d216.png
.. |75507b| image:: images/75507b.png
.. |888a85| image:: images/888a85.png
.. |8ae234| image:: images/8ae234.png
.. |8f5902| image:: images/8f5902.png
.. |a40000| image:: images/a40000.png
.. |ad7f8a| image:: images/ad7f8a.png
.. |babdb6| image:: images/babdb6.png
.. |c17d11| image:: images/c17d11.png
.. |c4a000| image:: images/c4a000.png
.. |cc0000| image:: images/cc0000.png
.. |ce5c00| image:: images/ce5c00.png
.. |d3d7cf| image:: images/d3d7cf.png
.. |e9b96e| image:: images/e9b96e.png
.. |edd400| image:: images/edd400.png
.. |eeeeec| image:: images/eeeeec.png
.. |ef2929| image:: images/ef2929.png
.. |f57900| image:: images/f57900.png
.. |fcaf3e| image:: images/fcaf3e.png
.. |fce94f| image:: images/fce94f.png

32
pyproject.toml Normal file
View File

@ -0,0 +1,32 @@
[build-system]
requires = ['flit_core >=2,<3']
build-backend = 'flit_core.buildapi'
[tool.flit.metadata]
module = 'brutalmaze'
author = 'Nguyễn Gia Phong'
author-email = 'mcsinyx@disroot.org'
home-page = 'https://git.disroot.org/McSinyx/brutalmaze'
requires = ['appdirs', 'palace', 'pygame>=1.9', 'setuptools']
description-file = 'README.rst'
classifiers = [
'Development Status :: 4 - Beta',
'Environment :: MacOS X',
'Environment :: Win32 (MS Windows)',
'Environment :: X11 Applications',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Games/Entertainment :: Arcade']
requires-python = '>=3.6'
keywords = 'pygame,shmup,maze,ai-challenges'
license = 'AGPLv3+'
[tool.flit.metadata.urls]
Documentation = 'https://brutalmaze.rtfd.io'
[tool.flit.entrypoints.console_scripts]
brutalmaze = 'brutalmaze.game:main'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -1,2 +0,0 @@
[bdist_wheel]
universal=1

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
with open('README.rst') as f:
long_description = f.read()
setup(
name='brutalmaze',
version='0.4.0',
description='A hash and slash game with fast-paced action and a minimalist art style',
long_description=long_description,
url='https://github.com/McSinyx/brutalmaze',
author='Nguyễn Gia Phong',
author_email='vn.mcsinyx@gmail.com',
license='GPLv3+',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: MacOS X',
'Environment :: Win32 (MS Windows)',
'Environment :: X11 Applications',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Games/Entertainment :: Arcade'],
keywords='pygame action-game arcade-game maze',
packages=['brutalmaze'],
install_requires=['pygame>=1.9'],
package_data={'brutalmaze': ['icon.png', 'soundfx/*.ogg']},
entry_points={'gui_scripts': ['brutalmaze = brutalmaze:main']})

21
tox.ini Normal file
View File

@ -0,0 +1,21 @@
[tox]
envlist = py
minversion = 3.3
isolated_build = True
[testenv]
deps =
flake8-builtins
isort
commands =
flake8
isort . --check --diff
[flake8]
hang-closing = True
ignore = E129, E226, E228, E701, E704, W503
exclude = .git,__pycache__,.tox,__init__.py
[isort]
balanced_wrapping = True
combine_as_imports = True