├── .gitignore
├── LICENSE
├── README.md
├── coderone
├── __init__.py
└── dungeon
│ ├── __init__.py
│ ├── agent.py
│ ├── agent_driver
│ ├── __init__.py
│ ├── agent.py
│ ├── module_watcher.py
│ ├── multiproc_driver.py
│ └── simple_driver.py
│ ├── arcade_client.py
│ ├── asset_manager.py
│ ├── assets
│ ├── images
│ │ ├── !!chest.png
│ │ ├── ammo.png
│ │ ├── big_demon_idle_anim_f0.png
│ │ ├── bomb_64px.png
│ │ ├── chest-danger.png
│ │ ├── chest.png
│ │ ├── chrome
│ │ │ ├── wall_corner_bottom_left.png
│ │ │ ├── wall_corner_bottom_right.png
│ │ │ ├── wall_corner_front_left.png
│ │ │ ├── wall_corner_front_right.png
│ │ │ ├── wall_corner_left.png
│ │ │ ├── wall_corner_right.png
│ │ │ ├── wall_corner_top_left.png
│ │ │ ├── wall_corner_top_right.png
│ │ │ ├── wall_left.png
│ │ │ ├── wall_mid.png
│ │ │ ├── wall_right.png
│ │ │ ├── wall_side_front_left.png
│ │ │ ├── wall_side_front_right.png
│ │ │ ├── wall_side_mid_left.png
│ │ │ ├── wall_side_mid_right.png
│ │ │ ├── wall_side_top_left.png
│ │ │ ├── wall_side_top_right.png
│ │ │ ├── wall_top_left.png
│ │ │ ├── wall_top_mid.png
│ │ │ └── wall_top_right.png
│ │ ├── coin_anim_f0.png
│ │ ├── crate.png
│ │ ├── explosion.png
│ │ ├── floor_1.png
│ │ ├── floor_2.png
│ │ ├── floor_3.png
│ │ ├── floor_4.png
│ │ ├── floor_5.png
│ │ ├── floor_6.png
│ │ ├── floor_7.png
│ │ ├── floor_8.png
│ │ ├── imp_idle_anim_f2.png
│ │ ├── knight.png
│ │ ├── lizard_m_run_anim_f0.png
│ │ ├── metal_block.png
│ │ ├── ore_block.png
│ │ ├── p1_knight_64px.png
│ │ ├── p2_knight_64px.png
│ │ ├── p2_knight_64px_flipped.png
│ │ ├── p2_knight_orange_64px_flipped.png
│ │ ├── skelet_run_anim_f1.png
│ │ ├── wall_banner_blue.png
│ │ ├── wall_banner_green.png
│ │ ├── wall_banner_red.png
│ │ ├── wall_banner_yellow.png
│ │ ├── wizard_f_64px.png
│ │ └── wizard_m_64px.png
│ └── sounds
│ │ └── explosion.mp3
│ ├── game.py
│ ├── game_recorder.py
│ ├── hack_client.py
│ ├── headless_client.py
│ ├── main.py
│ ├── publisher.py
│ └── requirements.txt
├── config.json
├── modular_agent
├── __init__.py
└── stand_still.py
├── random_agent.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
140 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > ⚠️ **Are you here for the 2021 AI Sports Challenge?** \
2 | This repo contains the game and documentation for the 2020 version of Dungeons and Data Structures. You can play around with it as practise, but a new game and documentation will be released at the start of the challenge on 22 April 5 PM (AEST). Stay up to date on challenge announcements by joining our [Discord](https://discord.gg/NkfgvRN) and checking your email (after registering [here](https://gocoder.one/aisports?utm_source=github)).
3 |
4 | # Dungeons & Data Structures
5 |
6 | **The AI Sports Challenge 2020 season has officially ended!**
7 | We had some amazing submissions from around the world. Congratulations to all our teams! 🎉
8 | If you created a bot, we'd love for you to share it with us by raising a pull request against this repo and adding the link to your bot for others to play against below. 👇
9 |
10 | ### Agent Directory
11 | - TEAM NAME
12 | - Baby Yoda
13 | - terserah
14 | - pBot Compilers
15 | - [Level 1 NPC](https://github.com/Jy4ng/coderone2020)
16 | - bruh
17 | - [ARX II-13](https://github.com/yukuefume/arx_ii-13)
18 | - M Zheng
19 | - KnAIght
20 | - A Tan
21 | - BombPyrates
22 | - Chaotic NULLtral
23 | - [Jigglybluff](https://github.com/garyleecf/jigglybluff_ozzymozzy)
24 | - RizDog
25 | - [PGB](https://github.com/GillesVandewiele/aisports-bomberman)
26 | - Rengoku
27 | - Monty Python 3.9.1
28 | - Reed-Solo-man Error Collector
29 | - [Bots by Robots](https://github.com/tannishpage/Coder_one_bot)
30 | - [Gambit](https://github.com/Jfbarr/Gambit-dijkstra-bot)
31 | - jab.AI
32 | - FleeBot2077
33 | - [Floth](https://github.com/suwat513/coderone-floth)
34 | - DLCT2
35 | - FSO
36 | - Oke
37 | - Mosin
38 | - Thailandnumberone
39 | - Bomber Clan
40 | - ZeroNet
41 | - thegeeky
42 | - AeroBot
43 | - Yeet Yate Yote
44 | - Donkie
45 | - [K Chan](https://github.com/kaleongchan/k-chan-agent)
46 | - [Team Solo Bot](https://github.com/christopherhb/faiker)
47 | - [Artificial Incompetence](https://github.com/gimait/DaDSbot)
48 | - SY
49 | - [Wizards](https://github.com/chrisrabe/ai-sports)
50 | - UC Engineering Society
51 | - Rootbeer
52 | - Datawizards
53 |
54 | # Setup and Installation
55 | > ⚠️ **New here?** \
56 | We **HIGHLY** recommend checking out the full documentation [here](https://bit.ly/aisportschallenge), which includes installation instructions, getting started tutorials, FAQ's, and tips and tricks.
57 |
58 | You're also welcome to join us on [Discord](https://discord.gg/NkfgvRN) to contribute to the discussion or ask any questions from the community.
59 |
60 | ## Prerequisites
61 | This is a `python` project and a valid Python3 installation is required to run it.
62 | If you don't have `python` installed on your machine, please go to the official Python web site [here](https://www.python.org/) and follow installation instructions for your operating system.
63 |
64 | ## Installation
65 | There are several ways to install the project. The simplest way is to use intallation package.
66 | Download the latest available release from the [github release page](https://github.com/gocoderone/dungeons-and-data-structures/releases).
67 |
68 | ### MacOS/Linux
69 | Assuming `~/workspace` is the directory you want to use to develop your Agent, open your terminal and run:
70 | ```shell
71 | # Create a working directory for your project:
72 | > mkdir -p ~/workspace/my-agent
73 |
74 | # Change into that directory
75 | > cd ~/workspace/my-agent
76 |
77 | # Create a python virtual environment. Let's call it venv
78 | > python3 -m venv venv
79 | # Activate your new python environment
80 | > source venv/bin/activate
81 |
82 | # Install Coder One Dungeon module you have previously downloaded:
83 | > pip install ~/Downloads/coderone-challenge-dungeons-0.1.0.tar.gz
84 |
85 | ```
86 | Once you have a working installation you can start working on your Agent:
87 | ```
88 | coderone-dungeon --interactive --watch my_agent
89 | ```
90 |
91 | ### Windows
92 | Windows installation follows similar steps as other operating system. For a step-by-step, check the full documentation [here](https://bit.ly/aisportschallenge).
93 | It is possible that you might need to use an alternative way to start the game, instead of relying on the convenience wrapper `codeone-dungeon`.
94 | ```shell
95 | > python -m coderone.dungeon.main
96 | ```
97 |
98 | ## Running a game
99 | Once all game dependencies have been properly installed, the game can be launched using command line:
100 | ```
101 | python -m coderone.dungeon.main
102 | ```
103 |
104 | There are a number of command line options supported by the game driver. To get a full list of options run:
105 | ```
106 | python -m coderone.dungeon.main --help
107 | ```
108 |
109 | ### Command line options summory
110 | The game runner recognises a number of command line options.
111 | Use `python -m coderone.dungeon.main --help` to get a list of supported options.
112 |
113 | * `--headless` - run the game without graphics. Tournament matches will be run in this mode.
114 | * `--interactive` - game is created with an extra player for the interactive user. This player can be controlled using your keyboard.
115 | * `--watch` - automatically reload user's Agent if source code files changes. This allows for interactive development as code can be edited while the game is running.
116 | * `--record ` - record game action into a specified file for later review.
117 |
118 | ### Interactive mode keys:
119 | * `Enter` - pause / un-pause the game
120 | * `r` - restart the game with a new random map
121 | * `↑` / `↓` / `←` / `→` - move
122 | * `` - place a bomb
123 |
124 | ### Game modes
125 | There are 3 main modes to run the game:
126 | - Interactive mode - when a human player can participate in a match
127 | This is a 'normal' game mode when a human user can play the game with the keyboard the game to explore it. This mode requires at least one bot to be specified.
128 | - Match - two or more* Agents play a game without a human participant.
129 | - 'Headless' match - the game is played by bots with no human participant and without graphics output.
130 | This is the mode used to run a tournament.
131 |
132 | By default, game runs in a tournament mode, without user input, with graphics output. For example, to run a random match between two Agents "agent1.py" and "agent2.py", run:
133 | ```
134 | coderone-dungeon agent1.py agent2.py
135 | ```
136 |
137 | Agents with multi-file code are also supported. Keep in mind that a proper [python module](https://docs.python.org/3/tutorial/modules.html) must include `__init__.py` file.
138 |
139 | ## Config options
140 | On the first run the game will generate a default config file `config.json` and store it in the OS-specific configuation directory.
141 |
142 | Default config looks like this:
143 | ```
144 | {
145 | "headless": false,
146 | "interactive": false,
147 | "start_paused": true,
148 | "wait_end": 5,
149 | "max_iterations": 3000,
150 | "tick_step": 0.10
151 | }
152 | ```
153 | ### Config notes
154 | In your local development environment you have access to all config options, such as number of iterations the game runs (`max_iterations`) or game update time step (`tick_step`). However, these options are fixed in the tournament and cannot be modified so please don't rely on these values.
155 |
156 |
157 | ## Known issues
158 | The Python library used for graphics has some known issues.
159 | If you experience a game crash with an error like:
160 | ```
161 | munmap_chunk(): invalid pointer
162 | Aborted (core dumped)
163 | ```
164 | Add the following option to your `config.json`
165 | ```
166 | ...
167 | "no_text": true,
168 | ...
169 | ```
170 |
171 | This options disables all text in the game which resolves library crashes.
172 |
173 | On ARM, python Arcade fails to install without FFI. Install it via:
174 | ```shell
175 | > sudo apt install libffi-dev libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
176 | libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
177 | libharfbuzz-dev libfribidi-dev libxcb1-dev
178 | ```
179 |
180 | ## Development
181 |
182 | Clone this git repository:
183 | ```
184 | git clone https://github.com/gocoderone/dungeons-and-data-structures
185 | ```
186 |
187 | Open terminal and run the following commands:
188 |
189 | ```shell
190 | > cd
191 | > python3 -m venv venv
192 | > source venv/bin/activate
193 | > pip install -r coderone/dungeon/requirements.txt
194 |
195 | ```
196 |
197 | ## Contributions
198 | As we work towards building the home for AI Sports, we welcome all feedback and contributions to help us improve the experience for participants.
199 |
200 | Feel free to [raise](https://github.com/gocoderone/dungeons-and-data-structures/issues/new/choose) an issue against this Repo if you experience any issues or would like to send a suggestion.
201 |
202 | ## Authors
203 | HUMANS at [Coder One](https://www.gocoder.one/):
204 | * Ivan aka [@abbyssoul](https://github.com/abbyssoul)
205 |
206 |
--------------------------------------------------------------------------------
/coderone/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/__init__.py
--------------------------------------------------------------------------------
/coderone/dungeon/__init__.py:
--------------------------------------------------------------------------------
1 | # from .main import main
--------------------------------------------------------------------------------
/coderone/dungeon/agent.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Dict, List, Tuple, Union, NamedTuple, Any, Optional
3 |
4 | Point = Tuple[int, int]
5 | PID = int
6 |
7 |
8 | class EntityTags(Enum):
9 | Ammo = "a"
10 | Treasure = 't'
11 | Bomb = "b"
12 |
13 | SoftBlock = 'sb'
14 | OreBlock = 'ob'
15 | IndestructibleBlock = 'ib'
16 |
17 |
18 | class GameState:
19 | """ A state of the game as viewed by an agent.
20 | All agent receive the state game state each step to base their decisions on.
21 | """
22 |
23 | def __init__(self, is_over:bool, tick_number:int, size:Point,
24 | game_map:Dict,
25 | ammo:List[Point],
26 | treasure:List[Point],
27 | bombs:List[Point],
28 | blocks:List[Tuple[EntityTags, Point]],
29 | players:List[Tuple[PID, Point]],
30 | ):
31 | self.is_over = is_over
32 | self.tick_number = tick_number
33 | self._size = size
34 | self._game_map = game_map
35 | self._treasure = treasure
36 | self._ammo = ammo
37 | self._bombs = bombs
38 | self._blocks = blocks
39 | self._players = players
40 |
41 |
42 | @property
43 | def size(self) -> Point:
44 | """Get the size of the map area as a 'Point' tuple
45 | """
46 | return self._size
47 |
48 | @property
49 | def ammo(self) -> List[Point]:
50 | return self._ammo
51 |
52 | @property
53 | def treasure(self) -> List[Point]:
54 | return self._treasure
55 |
56 | @property
57 | def bombs(self) -> List[Point]:
58 | """Get a list of bombs placed on the map.
59 | """
60 | return self._bombs
61 |
62 | @property
63 | def all_blocks(self) -> List[Point]:
64 | return [pos for tag, pos in self._blocks]
65 |
66 | @property
67 | def indestructible_blocks(self) -> List[Point]:
68 | return [pos for tag, pos in self._blocks if tag == EntityTags.IndestructibleBlock.value]
69 |
70 | @property
71 | def soft_blocks(self) -> List[Point]:
72 | return [pos for tag, pos in self._blocks if tag == EntityTags.SoftBlock.value]
73 |
74 | @property
75 | def ore_blocks(self) -> List[Point]:
76 | return [pos for tag, pos in self._blocks if tag == EntityTags.OreBlock.value]
77 |
78 | def is_in_bounds(self, location:Point) -> bool:
79 | return location[0] >= 0 and location[0] < self.size[0] and \
80 | location[1] >= 0 and location[1] < self.size[1]
81 |
82 | def _has_occupancy(self, location:Point) -> bool:
83 | return location[0] in self._game_map and location[1] in self._game_map[location[0]]
84 |
85 | def entity_at(self, location:Point) -> EntityTags:
86 | if not self.is_in_bounds(location):
87 | return None
88 |
89 | return self._game_map[location[0]][location[1]] if self._has_occupancy(location) else None
90 |
91 | def is_occupied(self, location:Point) -> bool:
92 | return self.entity_at(location) is not None
93 |
94 | def opponents(self, excluding_player_pid:PID=None):
95 | return [pos for pid, pos in self._players if excluding_player_pid is not None and pid != excluding_player_pid or excluding_player_pid is None]
96 |
97 |
98 | class PlayerState:
99 | def __init__(self, id:PID, ammo:int, hp:int, location:Point, reward:int, power:int):
100 | self.id = id
101 | self.ammo = ammo
102 | self.hp = hp
103 | self.location = location
104 | self.reward = reward
105 | self.power = power
106 |
107 |
108 | class Agent:
109 |
110 | def next_move(self, game_state:GameState, player_state:PlayerState):
111 | pass
112 |
113 | def on_game_over(self, game_state:GameState, player_state:PlayerState):
114 | pass
115 |
--------------------------------------------------------------------------------
/coderone/dungeon/agent_driver/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/agent_driver/__init__.py
--------------------------------------------------------------------------------
/coderone/dungeon/agent_driver/agent.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from ..agent import Agent as AIAgent, GameState, PlayerState
3 |
4 | logger = logging.getLogger(__name__)
5 |
6 |
7 | class Agent:
8 |
9 | def next_move(self):
10 | pass
11 |
12 | def update(self, game_state:GameState, player_state:PlayerState):
13 | pass
14 |
15 | def on_game_over(self, game_state:GameState, player_state:PlayerState):
16 | pass
17 |
18 |
19 | class AgentProxy(AIAgent):
20 | def __init__(self, module):
21 | self.agent = None
22 | self.reload(module)
23 |
24 | def next_move(self, game_state:GameState, player_state:PlayerState):
25 | try:
26 | return self.agent.next_move(game_state, player_state) if self.agent else None
27 | except Exception as e:
28 | # self.agent = None
29 | if not self.silinced:
30 | # self.silinced = True
31 | logger.error(f"Agent update error: {e}", exc_info=True)
32 | return None
33 |
34 | def on_game_over(self, game_state:GameState, player_state:PlayerState):
35 | try:
36 | return self.agent.on_game_over(game_state, player_state) if self.agent else None
37 | except Exception as e:
38 | # self.agent = None
39 | if not self.silinced:
40 | # self.silinced = True
41 | logger.error(f"Agent game_over error: {e}", exc_info=True)
42 | return None
43 |
44 | def reload(self, module):
45 | logger.debug("Re-loading proxy agent for module '%s'", module.__name__)
46 | try:
47 | self.silinced = False
48 | if hasattr(module, 'agent'):
49 | self.agent = module.agent()
50 | elif hasattr(module, 'Agent'):
51 | self.agent = module.Agent()
52 | else:
53 | self.agent = None
54 | logger.warn(f"No agent definition found in module '%'", module.__name__)
55 | except Exception as e:
56 | self.agent = None
57 | logger.error(f"Failed to reload agent: {e}", exc_info=True)
58 |
59 |
60 | class ModuleProxy:
61 | def __init__(self, module):
62 | self.agents = []
63 | self.on_reload(module)
64 |
65 | def agent(self) -> AIAgent:
66 | proxy = AgentProxy(self.module)
67 | self.agents.append(proxy)
68 | return proxy
69 |
70 | def on_reload(self, new_module):
71 | logger.debug("ModuleProxy reloaded")
72 |
73 | self.module = new_module
74 | for proxy in self.agents:
75 | proxy.reload(self.module)
76 |
--------------------------------------------------------------------------------
/coderone/dungeon/agent_driver/module_watcher.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import importlib
3 | from importlib.util import find_spec
4 | import os, sys
5 |
6 |
7 | from watchdog.observers import Observer
8 | from watchdog.events import FileSystemEventHandler
9 | from watchdog.events import RegexMatchingEventHandler
10 |
11 | logger = logging.getLogger(__name__)
12 | # logger.setLevel(logging.DEBUG)
13 |
14 | class FileEventHandler(FileSystemEventHandler):
15 | # class FileEventHandler(RegexMatchingEventHandler):
16 | PY_REGEX = [r".*\.py$"]
17 |
18 | def __init__(self, module, callback):
19 | # super().__init__(self.PY_REGEX)
20 | self.module = module
21 | self.callback = callback
22 |
23 | def on_modified(self, event):
24 | "A file of interest has changed"
25 | logger.debug(f"Changes {event.event_type}: {event.src_path}")
26 | # if event.path not in self.mod_map: # Is it a file I know about?
27 | # return
28 | if event.src_path.endswith('.py'):
29 | self.__reload(event)
30 |
31 | def __reload(self, event):
32 | # # Find out which module is using that file
33 | module_name = self.module.__name__ #self.mod_map[event.path]
34 |
35 | # Try to reload the module
36 | try:
37 | logger.info(f"re-loading module: '{module_name}'")
38 | new_module = importlib.reload(self.module)
39 | if self.callback:
40 | logger.debug("re-setting agents")
41 | self.callback(new_module)
42 |
43 | logger.info(f"module '{module_name}' reloaded")
44 | except Exception as e:
45 | logger.warning(f"Failed to re-load module: '{self.module}'")
46 | logger.error(e, exc_info=True)
47 |
48 |
49 | class ModuleWatcher():
50 | """ Automatically reload any modules or packages as they change
51 | """
52 |
53 | def __init__(self):
54 | self.__event_observer = Observer()
55 |
56 | def _watch_file(self, module, callback, file_name:str, search_locations):
57 | "Add a watch for a specific file, and map said file to a module name"
58 | recursive = True if search_locations else False
59 |
60 | file_name = os.path.realpath(file_name)
61 | if not os.path.isdir(file_name):
62 | file_name = os.path.dirname(file_name)
63 |
64 | logger.debug(f"watching for {file_name} changes, recursive={recursive}")
65 |
66 | self.__event_observer.schedule(
67 | FileEventHandler(module, callback),
68 | file_name,
69 | recursive=recursive
70 | )
71 |
72 | def watch_module(self, module, callback):
73 | "Load module spec, determine which files it uses, and watch them"
74 | logger.info(f"setting up module watcher for '{module}'")
75 | module_name = module.__name__
76 |
77 | try:
78 | spec = find_spec(module_name)
79 | if spec and spec.has_location:
80 | self._watch_file(module, callback, spec.origin, spec.submodule_search_locations)
81 | else:
82 | logger.info(f"...skipping watcher for '{module_name}' as no spec found")
83 | except Exception as e:
84 | logger.warning(f"Failed set up watcher for module '{module_name}'")
85 | logger.error(e, exc_info=True)
86 |
87 | def start_watching(self):
88 | "Start watch thread"
89 | logger.debug("Starting file watcher")
90 | self.__event_observer.start()
91 |
92 | def stop_watching(self):
93 | "Stop the watching thread"
94 | logger.debug("stopping file watcher")
95 | self.__event_observer.stop()
96 | self.__event_observer.join()
97 |
--------------------------------------------------------------------------------
/coderone/dungeon/agent_driver/multiproc_driver.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import time
3 | import logging
4 |
5 | from ..agent import Agent as AIAgent, GameState, PlayerState
6 | from .agent import Agent
7 | from .simple_driver import Driver as SimpleDriver
8 |
9 |
10 | logger = logging.getLogger(__name__)
11 | # logger.setLevel(logging.DEBUG)
12 |
13 |
14 | class StateUpdate:
15 | def __init__(self, game=None, player=None):
16 | self.game = game
17 | self.player = player
18 |
19 | class GameOver:
20 | def __init__(self, game=None, player=None):
21 | self.game = game
22 | self.player = player
23 |
24 | class AgentReady:
25 | pass
26 |
27 | class AgentProxy(Agent):
28 | MAX_READY_SPAM = 3
29 |
30 | def __init__(self, task_queue, result_queue, name:str):
31 | logger.debug("Creating multiproc agent proxy for %s", name)
32 | self.name = name
33 | self.task_queue = task_queue
34 | self.result_queue = result_queue
35 | self.silenced = False
36 |
37 | self.__is_ready = False
38 | self.__last_move = None
39 |
40 | @property
41 | def is_ready(self):
42 | if not self.__is_ready:
43 | # Pick a message:
44 | agent_message = self.result_queue.get_nowait() if not self.result_queue.empty() else None
45 | if isinstance(agent_message, AgentReady):
46 | self.__is_ready = True
47 | else:
48 | self.__last_move = agent_message
49 |
50 | return self.__is_ready
51 |
52 |
53 |
54 | def stop(self):
55 | # Put a poison pill to signal the stop to the agent driver
56 | self.task_queue.put(None)
57 |
58 | def next_move(self):
59 | for _ in range(self.MAX_READY_SPAM): # Give agent at most MAX_READY_SPAM attempts to report ready_state and start moving
60 | agent_message = self.result_queue.get_nowait() if not self.result_queue.empty() else None
61 | if isinstance(agent_message, AgentReady):
62 | self.__is_ready = True
63 | continue
64 |
65 | return agent_message
66 |
67 | return None
68 |
69 | def update(self, game_state:GameState, player_state:PlayerState):
70 | self.task_queue.put_nowait(StateUpdate(game=game_state, player=player_state))
71 |
72 | def on_game_over(self, game_state:GameState, player_state:PlayerState):
73 | self.task_queue.put_nowait(GameOver(game=game_state, player=player_state))
74 |
75 |
76 | class Consumer(multiprocessing.Process):
77 | def __init__(self, task_queue, result_queue, module_name:str, watch:bool, config):
78 | multiprocessing.Process.__init__(self, daemon=True)
79 | self.task_queue = task_queue
80 | self.result_queue = result_queue
81 | self.module_name = module_name
82 | self.watch = watch
83 | self.config = config
84 |
85 | self.is_not_done = True
86 | self.game_state = None
87 | self.player_state = None
88 |
89 | def _process_cmd(self, cmd):
90 | if not cmd: # Poison pill means shutdown
91 | self.is_not_done = True
92 | self.task_queue.close()
93 | self.result_queue.close()
94 | logger.debug(f'Agent {self.name}: Exiting')
95 | return False
96 |
97 | if isinstance(cmd, StateUpdate):
98 | self.game_state = cmd.game
99 | self.player_state = cmd.player
100 | else:
101 | logger.error(f"Unexpected command {cmd}")
102 |
103 | return True
104 |
105 |
106 | def run(self):
107 | driver = SimpleDriver(self.module_name, watch=self.watch, config=self.config)
108 |
109 | try:
110 | agent = driver.agent()
111 |
112 | # Report agent-ready status:
113 | self.result_queue.put(AgentReady())
114 |
115 | time_posted = time.time()
116 | while self.is_not_done:
117 | while not self.task_queue.empty():
118 | cmd = self.task_queue.get()
119 | if not self._process_cmd(cmd):
120 | continue
121 |
122 | if self.game_state and self.player_state:
123 | cycle_start_time = time.time()
124 |
125 | agent_action = agent.next_move(self.game_state, self.player_state)
126 |
127 | self.game_map = None
128 | self.game_state = None
129 |
130 | self.result_queue.put(agent_action)
131 |
132 | logger.debug(f"Time since last post: {cycle_start_time - time_posted}")
133 | time_posted = cycle_start_time
134 |
135 | except OSError:
136 | pass
137 |
138 | except KeyboardInterrupt:
139 | pass
140 |
141 | logger.debug(f"{self.name} loop is over")
142 | return
143 |
144 |
145 | class Driver:
146 |
147 | JOIN_TIMEOUT_SEC = 5
148 |
149 | def __init__(self, name:str, watch: bool = False, config={}):
150 | self.name = name
151 | self.is_ready = False
152 | self.watch = watch
153 | self.config = config
154 | self._proxies = []
155 | self._workers = []
156 |
157 | def stop(self):
158 | for p in self._proxies:
159 | p.stop()
160 |
161 | for w in self._workers:
162 | try:
163 | w.join(self.JOIN_TIMEOUT_SEC)
164 | w.close()
165 | except ValueError:
166 | logger.warn(f"process for agent '{self.name}' has not finished gracefully. Terminating")
167 | w.terminate()
168 |
169 | def agent(self) -> AgentProxy:
170 | tasks_queue = multiprocessing.Queue()
171 | agent_result_queue = multiprocessing.Queue()
172 | proxy = AgentProxy(tasks_queue, agent_result_queue, self.name)
173 |
174 | worker = Consumer(tasks_queue, agent_result_queue, self.name, self.watch, self.config)
175 | worker.start()
176 |
177 | self._workers.append(worker)
178 | self._proxies.append(proxy)
179 |
180 | return proxy
181 |
182 | def __enter__(self):
183 | return self
184 |
185 | def __exit__(self, exc_type, exc_value, traceback):
186 | self.stop()
187 |
--------------------------------------------------------------------------------
/coderone/dungeon/agent_driver/simple_driver.py:
--------------------------------------------------------------------------------
1 | import importlib
2 |
3 | from ..agent import Agent as AIAgent
4 | from .agent import ModuleProxy
5 | from .module_watcher import ModuleWatcher
6 |
7 | class Driver:
8 |
9 | def __init__(self, name:str, watch: bool = False, config=None):
10 | self.name = name
11 | self.watch = watch
12 | self.agent_module = None
13 | self.watcher = None
14 |
15 | def stop(self):
16 | if self.watcher:
17 | self.watcher.stop_watching()
18 |
19 | def agent(self) -> AIAgent:
20 | if not self.agent_module:
21 | module = importlib.import_module(self.name)
22 | self.agent_module = ModuleProxy(module)
23 |
24 | if self.watch:
25 | self.watcher = ModuleWatcher()
26 | self.watcher.watch_module(module, self.agent_module.on_reload)
27 | self.watcher.start_watching()
28 |
29 | return self.agent_module.agent() if self.agent_module else None
30 |
31 | def __enter__(self):
32 | return self
33 |
34 | def __exit__(self, exc_type, exc_value, traceback):
35 | self.stop()
36 |
--------------------------------------------------------------------------------
/coderone/dungeon/arcade_client.py:
--------------------------------------------------------------------------------
1 | import arcade
2 | from pyglet.gl import GL_NEAREST
3 |
4 | from .asset_manager import AssetManager, AssetType
5 | from .game import PlayerActions, Point, PID, Game
6 |
7 | # WIDTH and HEIGHT of each grid cell in pixels
8 | WIDTH = 64
9 | HEIGHT = 64
10 | PADDING = (int(WIDTH/2), HEIGHT)
11 |
12 |
13 | def grid_to_pos(pos:Point):
14 | x = PADDING[0] + pos[0] * WIDTH + (WIDTH / 2)
15 | y = PADDING[1] + pos[1] * HEIGHT + (HEIGHT / 2)
16 | return x,y
17 |
18 |
19 | class Sfx(arcade.Sprite):
20 | """ Special visual effects
21 | animation with no game mechanics impact
22 | """
23 |
24 | def __init__(self, loc:Point, texture_list):
25 | super().__init__()
26 | self.center_x,self.center_y = grid_to_pos(loc)
27 |
28 | # Start at the first frame
29 | self.current_texture = 0
30 | self.textures = texture_list
31 | self.set_texture(self.current_texture)
32 |
33 | def update(self):
34 | self.current_texture += 1
35 | if self.current_texture < len(self.textures):
36 | self.set_texture(self.current_texture)
37 | else:
38 | self.remove_from_sprite_lists()
39 |
40 | class StaticSprite(arcade.Sprite):
41 | def __init__(self, asset, owner, scale):
42 | super().__init__(asset, scale)
43 | self.owner = owner
44 | x, y = grid_to_pos(self.owner.pos)
45 | self.set_position(x, y)
46 |
47 | def update_pos(self):
48 | x, y = grid_to_pos(self.owner.pos)
49 | self.set_position(x, y)
50 |
51 | class Player(StaticSprite):
52 | """ Player Class """
53 | def __init__(self, asset, owner):
54 | super().__init__(asset, owner, scale=1.0)
55 |
56 | def update(self):
57 | """ Move the player """
58 | self.update_pos()
59 | if not self.owner.is_alive:
60 | self.remove_from_sprite_lists()
61 |
62 |
63 | class Client(arcade.Window):
64 | """
65 | Main application class.
66 | """
67 |
68 | def _add_wall(self, col, row, asset):
69 | tile = arcade.Sprite(asset, 4)
70 | x,y = grid_to_pos((col,row))
71 | tile.set_position(x,y)
72 | self.chrome_tiles.append(tile)
73 |
74 | def __init__(self, width:int, height:int, title:str, game:Game, config, interactive:bool, user_pid:PID):
75 | """
76 | Set up the application.
77 | """
78 | super().__init__(width, height, title)
79 |
80 | self.app_config = config
81 | self.asset_man = AssetManager(config.get('assets'))
82 | self.game = game
83 | self.user_pid = user_pid
84 | self.interactive = interactive
85 | self.paused = self.app_config.get('start_paused', False)
86 | self.single_step = self.app_config.get('single_step', False)
87 | self.is_endless = self.app_config.get('endless', False)
88 | self.end_game_wait_time = self.app_config.get('wait_end')
89 | self.end_game_timer = self.end_game_wait_time or 0
90 |
91 | arcade.set_background_color(arcade.color.BLACK)
92 |
93 |
94 | # Pre-load the animation frames. We don't do this in the __init__
95 | # of the explosion sprite because it
96 | # takes too long and would cause the game to pause.
97 | columns = 16
98 | count = 60
99 | sprite_width = 256
100 | sprite_height = 256
101 |
102 | # Load the explosions from a sprite sheet
103 | self.explosion_texture_list = arcade.load_spritesheet(self.asset_man.explosion, sprite_width, sprite_height, columns, count)
104 |
105 | # Loading explostions sound
106 | self.hit_sound = arcade.sound.load_sound(self.asset_man.explosion_sound)
107 |
108 | self.chrome_tiles = arcade.SpriteList()
109 | self._add_wall(-1, self.game.row_count + 1, self.asset_man.asset("chrome/wall_side_top_left.png", AssetType.IMAGE))
110 | self._add_wall(self.game.column_count, self.game.row_count + 1, self.asset_man.asset("chrome/wall_side_top_right.png", AssetType.IMAGE))
111 |
112 | self._add_wall(-1, -1, self.asset_man.asset("chrome/wall_side_front_left.png", AssetType.IMAGE))
113 | self._add_wall(self.game.column_count, -1, self.asset_man.asset("chrome/wall_side_front_right.png", AssetType.IMAGE))
114 |
115 | self._add_wall(0, self.game.row_count, self.asset_man.asset("chrome/wall_corner_front_left.png", AssetType.IMAGE))
116 | self._add_wall(0, self.game.row_count + 1, self.asset_man.asset("chrome/wall_corner_top_left.png", AssetType.IMAGE))
117 |
118 | self._add_wall(self.game.column_count - 1, self.game.row_count, self.asset_man.asset("chrome/wall_corner_front_right.png", AssetType.IMAGE))
119 | self._add_wall(self.game.column_count - 1, self.game.row_count+1, self.asset_man.asset("chrome/wall_corner_top_right.png", AssetType.IMAGE))
120 |
121 | self._add_wall(0, 0, self.asset_man.asset("chrome/wall_top_left.png", AssetType.IMAGE))
122 | self._add_wall(self.game.column_count - 1, 0, self.asset_man.asset("chrome/wall_top_right.png", AssetType.IMAGE))
123 | for column in range(1, self.game.column_count - 1):
124 | self._add_wall(column, self.game.row_count+1, self.asset_man.asset("chrome/wall_top_mid.png", AssetType.IMAGE))
125 | self._add_wall(column, 0, self.asset_man.asset("chrome/wall_top_mid.png", AssetType.IMAGE))
126 | self._add_wall(column, self.game.row_count, self.asset_man.asset("chrome/wall_mid.png", AssetType.IMAGE))
127 |
128 |
129 | for column in range(0, self.game.column_count):
130 | self._add_wall(column, -1, self.asset_man.asset("chrome/wall_mid.png", AssetType.IMAGE))
131 |
132 | for row in range(0, self.game.row_count+1):
133 | self._add_wall(-1, row, self.asset_man.asset("chrome/wall_side_mid_left.png", AssetType.IMAGE))
134 | self._add_wall(self.game.column_count, row, self.asset_man.asset("chrome/wall_side_mid_right.png", AssetType.IMAGE))
135 |
136 | self._map_game()
137 |
138 | def _add_blocks(self, asset, blocks, scale=4):
139 | self.block_list.extend(map(lambda block: StaticSprite(asset, block, scale), blocks))
140 |
141 | def _map_game(self):
142 | self.player_list = arcade.SpriteList()
143 | self.grid_sprite_list = arcade.SpriteList()
144 | self.block_list = arcade.SpriteList()
145 | self.sfx_list = arcade.SpriteList()
146 |
147 | # Create a list of solid-color sprites to represent each grid location
148 | for column in range(self.game.column_count):
149 | for row in range(self.game.row_count):
150 | sprite = arcade.Sprite(self.asset_man.floor_tile, 4)
151 | x,y = grid_to_pos((column, row))
152 | sprite.set_position(x, y)
153 | self.grid_sprite_list.append(sprite)
154 |
155 | # Create player sprites
156 | self.player_list.extend([Player(self.asset_man.player_avatar(pid), player) for pid, player in self.game.players.items()])
157 |
158 | self._add_blocks(self.asset_man.indestructible_block, self.game.static_block_list)
159 | self.block_list.extend(map(lambda block: StaticSprite(self.asset_man.ore_block if block.hp > 1 else self.asset_man.soft_block, block, 4.0), self.game.value_block_list))
160 |
161 | self._add_blocks(self.asset_man.ammunition, self.game.ammunition_list, 1)
162 | self._add_blocks(self.asset_man.treasure, self.game.treasure_list, 1)
163 | self._add_blocks(self.asset_man.bomb, self.game.bomb_list, 1)
164 | self._add_blocks(self.asset_man.fire, self.game.fire_list)
165 | self._add_blocks(self.asset_man.skeleton, self.game.dead_player_list)
166 |
167 | def _update_map(self):
168 | # Remove all the blocks that are not in game
169 | to_remove = [sprite for sprite in self.block_list if sprite.owner not in self.game.all_entities]
170 | for sprite in to_remove:
171 | sprite.remove_from_sprite_lists()
172 |
173 | all_block_owner = [sprite.owner for sprite in self.block_list]
174 | self._add_blocks(self.asset_man.ammunition, [block for block in self.game.ammunition_list if block not in all_block_owner], 1)
175 | self._add_blocks(self.asset_man.treasure, [block for block in self.game.treasure_list if block not in all_block_owner], 1)
176 | self._add_blocks(self.asset_man.bomb, [block for block in self.game.bomb_list if block not in all_block_owner], 1)
177 | self._add_blocks(self.asset_man.fire, [block for block in self.game.fire_list if block not in all_block_owner])
178 | self._add_blocks(self.asset_man.skeleton, [block for block in self.game.dead_player_list if block not in all_block_owner])
179 |
180 | new_fire = [block for block in self.game.fire_list if block not in all_block_owner]
181 | self._add_blocks(self.asset_man.fire, new_fire)
182 |
183 | # Add Fire-SFX for each new fire position
184 | self.sfx_list.extend(map(lambda fire: Sfx(fire.pos, self.explosion_texture_list), new_fire))
185 |
186 | def tick_game(self, time_interval):
187 | if not self.paused: # if game has paused then stop
188 |
189 | self.game.tick(time_interval)
190 |
191 | if self.game.is_over and self.is_endless:
192 | self._reset_game()
193 |
194 | if self.single_step:
195 | self.paused = True
196 |
197 | def run(self, time_interval):
198 | # Enable fixed interval timer
199 | arcade.schedule(self.tick_game, time_interval)
200 | arcade.run()
201 |
202 | def on_draw(self):
203 | """ Render the screen """
204 | # This command has to happen before we start drawing
205 | arcade.start_render()
206 |
207 | self.grid_sprite_list.draw(filter=GL_NEAREST)
208 | self.block_list.draw(filter=GL_NEAREST)
209 |
210 | #drawing player
211 | self.player_list.draw(filter=GL_NEAREST)
212 |
213 | self.sfx_list.draw()
214 |
215 | self.chrome_tiles.draw(filter=GL_NEAREST)
216 |
217 | # Print scores on the screen
218 | d_height = 20
219 | current_text_height = self.height - d_height
220 | if not self.app_config.get('no_text', False):
221 | for pid, player in self.game.players.items():
222 | name = "{}{}".format(player.name, '(bot)' if self.game.is_bot(pid) else "")
223 | player_output = f"{name} HP: {player.hp:3d} / Ammo: {player.ammo:3d} / Score: {player.reward:4d}"
224 |
225 | arcade.draw_text(player_output, 10, current_text_height, arcade.color.WHITE, 14, font_name='arial')
226 | current_text_height -= d_height
227 |
228 | if self.paused and not self.app_config.get('no_text', False):
229 | arcade.draw_text("PAUSED", self.width / 2, self.height / 2, arcade.color.WHITE, 42, bold=True,
230 | font_name='arial',
231 | align="center", anchor_x="center", anchor_y="center")
232 |
233 | if self.game.is_over and self.end_game_wait_time:
234 | progress = 360*(1 - self.end_game_timer / self.end_game_wait_time)
235 | sq_size = self.height / 4
236 | width = sq_size / 4
237 | arcade.draw_arc_outline(center_x=self.width / 2, center_y=self.height / 2,
238 | width=sq_size, height=sq_size, color=arcade.color.CYAN,
239 | border_width=width,
240 | start_angle=0, end_angle=progress,
241 | tilt_angle=90)
242 |
243 | # if not self.app_config.get('no_text', False):
244 | # arcade.draw_text(f"GAME OVER\n{self.end_game_timer:2.1f}",
245 | # self.width / 2, self.height / 2, arcade.color.BLACK, 42, bold=True,
246 | # font_name='arial',
247 | # align="center", anchor_x="center", anchor_y="center")
248 |
249 | #arcade.finish_render()
250 |
251 | def on_update(self, delta_time):
252 | """ Update game state """
253 |
254 | if not self.paused and self.game.is_over: # Game over count-down
255 | self.end_game_timer -= delta_time
256 |
257 | if self.end_game_timer < 0:
258 | self.close()
259 | return
260 |
261 | # Normal game update
262 | self.player_list.update()
263 | self.sfx_list.update()
264 |
265 | if not self.paused: # if game has paused then don't update it
266 | self._update_map()
267 |
268 |
269 | def _reset_game(self):
270 | self.end_game_timer = self.end_game_wait_time or 0
271 | self.game.generate_map()
272 | self._map_game()
273 |
274 | def on_key_press(self, key, modifiers):
275 | """Called whenever a key is pressed. """
276 |
277 | if key == arcade.key.ENTER:
278 | self.paused = not self.paused
279 |
280 | if self.interactive and key == arcade.key.R:
281 | self.paused = True
282 | self._reset_game()
283 |
284 | # Next command are only accepted if game is not paused:
285 | if self.paused or not self.interactive or not self.user_pid:
286 | return
287 |
288 | if key == arcade.key.UP or key == arcade.key.W:
289 | self.game.enqueue_action(self.user_pid, PlayerActions.MOVE_UP)
290 | elif key == arcade.key.DOWN or key == arcade.key.D:
291 | self.game.enqueue_action(self.user_pid, PlayerActions.MOVE_DOWN)
292 | elif key == arcade.key.LEFT or key == arcade.key.A:
293 | self.game.enqueue_action(self.user_pid, PlayerActions.MOVE_LEFT)
294 | elif key == arcade.key.RIGHT or key == arcade.key.D:
295 | self.game.enqueue_action(self.user_pid, PlayerActions.MOVE_RIGHT)
296 | elif key == arcade.key.SPACE:
297 | self.game.enqueue_action(self.user_pid, PlayerActions.PLACE_BOMB)
298 |
--------------------------------------------------------------------------------
/coderone/dungeon/asset_manager.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | import os
3 | import random
4 | import pkgutil
5 |
6 | class AssetType(Enum):
7 | IMAGE = 'images'
8 | SOUND = 'sounds'
9 |
10 | class AssetManager:
11 |
12 | # Images
13 | AMMUNITION_IMAGE = "ammo.png"
14 | TREASURE_CHEST_IMAGE = "chest.png"
15 | BOMB_IMAGE = "bomb_64px.png"
16 |
17 | SOFT_BLOCK_IMAGE = "crate.png"
18 | ORE_BLOCK_IMAGE = "ore_block.png"
19 | INDESTRUCTABLE_BLOCK_IMAGE = "metal_block.png"
20 |
21 | SKELETON_IMAGE = "skelet_run_anim_f1.png"
22 | FIRE_IMAGE = "coin_anim_f0.png"
23 | EXPLOSION_IMAGE = "explosion.png"
24 |
25 | # Sounds
26 | EXP_SOUND = "explosion.mp3"
27 |
28 | FLOOR_TILES = ["floor_1.png", "floor_2.png", "floor_3.png", "floor_4.png", "floor_5.png", "floor_6.png", "floor_7.png", "floor_8.png"]
29 | PLAYER_AVATARS = [
30 | "wizard_m_64px.png",
31 | "p1_knight_64px.png",
32 | "wizard_f_64px.png",
33 | "p2_knight_64px_flipped.png",
34 | "p2_knight_64px.png",
35 | "p2_knight_orange_64px_flipped.png",
36 | ]
37 |
38 | def __init__(self, asset_dir:str):
39 | self.asset_dir = asset_dir
40 |
41 | @property
42 | def explosion(self):
43 | return self.asset(self.EXPLOSION_IMAGE, AssetType.IMAGE)
44 |
45 | @property
46 | def floor_tile(self):
47 | tile = random.choice(self.FLOOR_TILES)
48 | return self.asset(tile, AssetType.IMAGE)
49 |
50 | @property
51 | def ammunition(self):
52 | return self.asset(self.AMMUNITION_IMAGE, AssetType.IMAGE)
53 |
54 | @property
55 | def treasure(self):
56 | return self.asset(self.TREASURE_CHEST_IMAGE, AssetType.IMAGE)
57 |
58 | @property
59 | def bomb(self):
60 | return self.asset(self.BOMB_IMAGE, AssetType.IMAGE)
61 |
62 | @property
63 | def indestructible_block(self):
64 | return self.asset(self.INDESTRUCTABLE_BLOCK_IMAGE, AssetType.IMAGE)
65 |
66 | @property
67 | def soft_block(self):
68 | return self.asset(self.SOFT_BLOCK_IMAGE, AssetType.IMAGE)
69 |
70 | @property
71 | def ore_block(self):
72 | return self.asset(self.ORE_BLOCK_IMAGE, AssetType.IMAGE)
73 |
74 | @property
75 | def skeleton(self):
76 | return self.asset(self.SKELETON_IMAGE, AssetType.IMAGE)
77 |
78 | @property
79 | def fire(self):
80 | return self.asset(self.FIRE_IMAGE, AssetType.IMAGE)
81 |
82 | @property
83 | def explosion_sound(self):
84 | return self.asset(self.EXP_SOUND, AssetType.SOUND)
85 |
86 |
87 | def player_avatar(self, pid:int):
88 | avatar = self.PLAYER_AVATARS[pid % len(self.PLAYER_AVATARS)]
89 | return self.asset(avatar, AssetType.IMAGE)
90 |
91 |
92 | def asset(self, name, assetType: AssetType):
93 | # data = pkgutil.get_data(__name__, "templates/temp_file")
94 | return os.path.join(self.asset_dir, assetType.value, name)
95 |
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/!!chest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/!!chest.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/ammo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/ammo.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/big_demon_idle_anim_f0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/big_demon_idle_anim_f0.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/bomb_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/bomb_64px.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chest-danger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chest-danger.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chest.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_bottom_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_bottom_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_bottom_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_bottom_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_front_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_front_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_front_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_front_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_top_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_top_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_corner_top_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_corner_top_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_mid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_mid.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_side_front_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_side_front_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_side_front_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_side_front_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_side_mid_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_side_mid_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_side_mid_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_side_mid_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_side_top_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_side_top_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_side_top_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_side_top_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_top_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_top_left.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_top_mid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_top_mid.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/chrome/wall_top_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/chrome/wall_top_right.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/coin_anim_f0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/coin_anim_f0.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/crate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/crate.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/explosion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/explosion.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_1.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_2.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_3.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_4.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_5.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_6.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_7.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/floor_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/floor_8.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/imp_idle_anim_f2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/imp_idle_anim_f2.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/knight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/knight.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/lizard_m_run_anim_f0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/lizard_m_run_anim_f0.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/metal_block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/metal_block.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/ore_block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/ore_block.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/p1_knight_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/p1_knight_64px.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/p2_knight_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/p2_knight_64px.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/p2_knight_64px_flipped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/p2_knight_64px_flipped.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/p2_knight_orange_64px_flipped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/p2_knight_orange_64px_flipped.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/skelet_run_anim_f1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/skelet_run_anim_f1.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/wall_banner_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/wall_banner_blue.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/wall_banner_green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/wall_banner_green.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/wall_banner_red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/wall_banner_red.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/wall_banner_yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/wall_banner_yellow.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/wizard_f_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/wizard_f_64px.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/images/wizard_m_64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/images/wizard_m_64px.png
--------------------------------------------------------------------------------
/coderone/dungeon/assets/sounds/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderOneHQ/dungeons-and-data-structures/204f411dd274221457cfbf3a6a54eb334867604b/coderone/dungeon/assets/sounds/explosion.mp3
--------------------------------------------------------------------------------
/coderone/dungeon/game.py:
--------------------------------------------------------------------------------
1 | import random
2 | import logging
3 |
4 | from enum import Enum
5 | from typing import Dict, List, Tuple, Union, NamedTuple, Any, Optional
6 | # from dataclasses import dataclass
7 | from collections import defaultdict
8 |
9 | from .agent import Agent, Point, PID, EntityTags, GameState, PlayerState
10 |
11 | logger = logging.getLogger(__name__)
12 | # logger.setLevel(logging.DEBUG)
13 |
14 |
15 | class DelayedEffectType(Enum):
16 | SPAWN_AMMO = 'a'
17 | SPAWN_TREASURE = 't'
18 |
19 | class PlayerActions(Enum):
20 | NO_OP = ''
21 | MOVE_UP = 'u'
22 | MOVE_DOWN = 'd'
23 | MOVE_LEFT = 'l'
24 | MOVE_RIGHT = 'r'
25 | PLACE_BOMB = 'b'
26 |
27 | class GameSysActions(Enum):
28 | MAP = "map" # A new map generated
29 | PLAYER_ADDED = "add_player" # Add a new player to the game
30 |
31 | class GameSysAction(NamedTuple):
32 | action: GameSysActions
33 | payload: Any
34 |
35 | class PlayerMove(NamedTuple):
36 | pid: PID
37 | action: PlayerActions
38 |
39 | GameEvent = Union[GameSysActions, PlayerMove]
40 |
41 |
42 | # @dataclass
43 | class PlayerStat(NamedTuple):
44 | name: str
45 | is_bot: bool
46 | score: int
47 | hp: int
48 | ammo: int
49 | position: Point
50 |
51 | # @dataclass
52 | class GameStats(NamedTuple):
53 | is_over: bool
54 | iteration:int
55 | winner_pid: PID
56 | players: Dict[PID, PlayerStat]
57 |
58 |
59 | def collide(pos1:Point, pos2:Point) -> bool:
60 | return (pos1[0] == pos2[0]) and (pos1[1] == pos2[1])
61 |
62 |
63 | class Recorder:
64 | def record(self, tick:int, event:GameEvent):
65 | pass
66 |
67 | def __enter__(self):
68 | return self
69 |
70 | def __exit__(self, exc_type, exc_value, traceback):
71 | pass
72 |
73 |
74 |
75 | class Game:
76 | """Game class defining game rules
77 | """
78 | # Set how many rows and columns we will have
79 | ROW_COUNT = 10
80 | COLUMN_COUNT = 12
81 |
82 | # We have total 12x10 = 120 cells
83 | STATIC_BLOCK_COUNT = 18 # 15% of the board are Indestructible blocks
84 | SOFT_BLOCK_COUNT = 20 # 20% of the board are low-value destructable blocks
85 | ORE_BLOCK_COUNT = 5 # 5% of the board are Ore blocks
86 |
87 | PLAYER_START_AMMO = 3 # Amount of ammo a player starts the match with
88 | FREE_AMMO_COUNT = 1 # Amount of free ammo, Should be Number of players - 1 - to create resource scarcity
89 |
90 | # Rewards and punishment
91 | FIRE_PENALTY = 0
92 | FIRE_REWARD = 25
93 |
94 | TREASURE_REWARD = 1
95 | SOFT_BLOCK_REWARD = 2
96 | ORE_BLOCK_REWARD = 10
97 |
98 | PLAYER_START_HP = 3 # Initial hp of a new player
99 | SOFTBLOCK_HP = 1 # Initial hp of a soft block
100 | ORE_BLOCK_HP = 3 # Initial hp of a ore block
101 |
102 | FIRE_HIT = 1 # Number of hit points taken by the fire
103 |
104 | PLAYER_START_POWER = 2 # Initial blast radius
105 | BOMB_TTL = 35 # Number of turns before bomb expires
106 |
107 | AMMO_PERISH_TTL = 5*BOMB_TTL # Number of turns before ammo expires
108 | AMMO_RESPAWN_TTL = 2*BOMB_TTL # Number of turns before ammo respawns
109 | TREASURE_SPAWN_FREQUENCY_MIN = 5*10 # Once every 180 steps
110 | TREASURE_SPAWN_FREQUENCY_MAX = 25*10 # Once every 180 steps
111 |
112 |
113 | ACTION_CODES = {
114 | 'u': PlayerActions.MOVE_UP,
115 | 'd': PlayerActions.MOVE_DOWN,
116 | 'l': PlayerActions.MOVE_LEFT,
117 | 'r': PlayerActions.MOVE_RIGHT,
118 | 'p': PlayerActions.PLACE_BOMB,
119 | 'b': PlayerActions.PLACE_BOMB,
120 | 'can_haz_boom': PlayerActions.PLACE_BOMB,
121 | }
122 |
123 | class _Positioned:
124 | def __init__(self, pos: Point):
125 | self.pos = pos
126 |
127 | class _Destructable(_Positioned):
128 | def __init__(self, ttl:int, pos: Point):
129 | super().__init__(pos)
130 | self._ttl = ttl
131 |
132 | @property
133 | def is_alive(self):
134 | return self._ttl > 0
135 |
136 | @property
137 | def hp(self):
138 | return self._ttl
139 |
140 | def apply_hit(self, delta_ttl:int) -> int:
141 | self._ttl -= delta_ttl
142 | return self._ttl
143 |
144 | def update(self) -> int:
145 | return self._ttl
146 |
147 | class _Perishable(_Destructable):
148 | def update(self) -> int:
149 | return self.apply_hit(1)
150 |
151 | class _DelayedEffect(_Perishable):
152 | def __init__(self, effect: DelayedEffectType, ttl:int):
153 | super().__init__(ttl=ttl, pos=None)
154 | self.effect = effect
155 |
156 | class _OwnedPositionedPerishable(_Perishable):
157 | def __init__(self, owner_id, pos: Point, ttl:int=1):
158 | super().__init__(pos=pos, ttl=ttl)
159 | self.owner_id = owner_id
160 |
161 | class _DeadBody(_Positioned):
162 | def __init__(self, pid, pos: Point):
163 | super().__init__(pos=pos)
164 | self.pid = pid
165 |
166 | class _Player(_Destructable):
167 | def __init__(self, hp:int, pos:Point, ammo:int, name:str, power:int, reward:int=0):
168 | super().__init__(pos=pos, ttl=hp)
169 | self.name = name
170 | self.ammo = ammo
171 | self.reward = reward
172 | self.power = power
173 |
174 |
175 | class _Ammunitation(_Perishable):
176 | Tag = EntityTags.Ammo.value
177 |
178 | def __init__(self, pos: Point, ttl: int, value: int=1, on_perish=None):
179 | super().__init__(pos=pos, ttl=ttl)
180 | self.value = value
181 | self.on_perish = on_perish
182 |
183 | @property
184 | def is_alive(self):
185 | return super().is_alive and self.value > 0
186 |
187 | def update(self):
188 | super().update()
189 | if not self.hp and self.on_perish:
190 | self.on_perish()
191 |
192 |
193 | class _Treasure(_Destructable):
194 | Tag = EntityTags.Treasure.value
195 |
196 | def __init__(self, pos: Point, value: int=None, ttl=1):
197 | super().__init__(pos=pos, ttl=1)
198 | self.value = value or Game.TREASURE_REWARD
199 |
200 | @property
201 | def is_alive(self):
202 | return super().is_alive and self.value > 0
203 |
204 | def update(self):
205 | pass
206 |
207 |
208 | class _Bomb(_OwnedPositionedPerishable):
209 | Tag = EntityTags.Bomb.value
210 |
211 | def __init__(self, owner_id:PID, pos:Point, ttl:int, power:int):
212 | super().__init__(owner_id=owner_id, pos=pos, ttl=ttl)
213 | self.power = power
214 |
215 | class _Fire(_OwnedPositionedPerishable):
216 | pass
217 |
218 | class _IndestructibleBlock(_Positioned):
219 | Tag = EntityTags.IndestructibleBlock.value
220 |
221 | class _SoftBlock(_Destructable):
222 | Tag = EntityTags.SoftBlock.value
223 |
224 | def __init__(self, pos:Point, hp:int):
225 | super().__init__(pos=pos, ttl=hp)
226 | self.reward = Game.SOFT_BLOCK_REWARD
227 |
228 | class _OreBlock(_Destructable):
229 | Tag = EntityTags.OreBlock.value
230 |
231 | def __init__(self, pos:Point, hp:int):
232 | super().__init__(pos=pos, ttl=hp)
233 | self.reward = Game.ORE_BLOCK_REWARD
234 |
235 |
236 | def __init__(self, row_count=ROW_COUNT, column_count=COLUMN_COUNT, max_iterations=None, recorder=Recorder()):
237 | self.row_count = row_count
238 | self.column_count = column_count
239 | self.recorder = recorder
240 |
241 | self.ACTION_CODES.setdefault(None)
242 |
243 | self.max_iterations = max_iterations
244 | self._pid_counter = 0
245 | self._agents:Dict[PID, Agent] = {}
246 |
247 | self.players:Dict[PID, self._Player] = {}
248 |
249 | self._reset_state()
250 |
251 |
252 | def add_agent(self, agent:Agent, name: Optional[str]) -> PID:
253 | """ Add new agent and a corresponding player to play the game
254 | """
255 | pid = self.add_player(name)
256 |
257 | self._agents[pid] = agent
258 |
259 | return pid
260 |
261 | def add_player(self, name: Optional[str]) -> PID:
262 | """ Add a new player to play the game
263 | """
264 | player_id = self._next_pid()
265 | name = name or f"P[{player_id}]"
266 | self.players[player_id] = self._Player(hp=self.PLAYER_START_HP, pos=None, ammo=self.PLAYER_START_AMMO, name=name, power=self.PLAYER_START_POWER)
267 |
268 | self.recorder.record(self.tick_counter, GameSysAction(GameSysActions.PLAYER_ADDED, name))
269 |
270 | return player_id
271 |
272 | def is_bot(self, pid:int) -> bool:
273 | """ Test if a give player_id belongs to the bot or a human player
274 | """
275 | return pid in self._agents
276 |
277 |
278 | def enqueue_action(self, pid:PID, action: PlayerActions):
279 | """ Add an action to the queue of player's actions to be executed during next game tick
280 | """
281 | if not action:
282 | return
283 |
284 | self._action_queue[pid].append(action)
285 |
286 |
287 | def tick(self, dt:float):
288 | """ Advance the state of the game by one step, which is dt sec in time.
289 | First, all agents are queried for their inputs based on the serialized state of the game.
290 | Then all enqueed player actions are applied first and object positions are updated accordingly.
291 |
292 | """
293 | if not self.is_over:
294 | # Gather commands from agents
295 | for pid, agent in self._agents.items():
296 | action = self._get_agent_input(pid, agent)
297 | if action:
298 | self.enqueue_action(pid, action)
299 |
300 | # Collect 1 enqueued action from each player for execution
301 | orders_for_tick = []
302 | for pid, action_queue in self._action_queue.items():
303 | if action_queue:
304 | action = action_queue.pop(0)
305 | orders_for_tick.append((pid, action))
306 |
307 | # Randomize the order in which actions appied.
308 | # This compemsates for low resolution of 100ms where informaion about exact timing of commands is lost.
309 | random.shuffle(orders_for_tick)
310 | for pid, action in orders_for_tick:
311 | self._apply_action(pid, action)
312 |
313 | # Apply fire!
314 | ## Check if any player stepped into a fire-zone
315 | for pid, player in self._alive_players():
316 | hit_list = self._collision_list(player.pos, self.fire_list)
317 | for hit in hit_list:
318 | fire_owner = self.players[hit.owner_id] if hit.owner_id in self.players else None
319 |
320 | # Apply fire damage to the player
321 | player.apply_hit(self.FIRE_HIT)
322 |
323 | player.reward -= self.FIRE_PENALTY
324 | if player != fire_owner and fire_owner:
325 | fire_owner.reward += self.FIRE_REWARD
326 |
327 |
328 | ## Apply fire damage to static entities
329 | for fire in self.fire_list:
330 | fire_owner = self.players[fire.owner_id] if fire.owner_id in self.players else None
331 |
332 | ## Check for fire damage of static blocks and collect rewads
333 | hitblock_list = self._collision_list(fire.pos, self.value_block_list)
334 | for block in hitblock_list:
335 | block.apply_hit(self.FIRE_HIT)
336 | if not block.is_alive and fire_owner:
337 | fire_owner.reward += block.reward
338 |
339 | ## Check for fire damage of nearby bombs and set them off
340 | hitbomb_list = self._collision_list(fire.pos, self.bomb_list)
341 | for bomb in hitbomb_list:
342 | bomb.apply_hit(bomb.hp)
343 |
344 | # Alive players get to pickup static rewards:
345 | for pid, player in self._alive_players():
346 | if player.is_alive: # Dead players are not allowed to pickup items
347 | # Pickup ammo:
348 | ammo_found = self._collision_list(player.pos, self.ammunition_list)
349 | for am in ammo_found:
350 | player.ammo += am.value
351 | am.value = 0
352 |
353 | # Pickup treasures:
354 | treasure_found = self._collision_list(player.pos, self.treasure_list)
355 | for treasure in treasure_found:
356 | player.reward += treasure.value
357 | treasure.value = 0
358 |
359 | # Update effects and lists
360 | self.__update_list(self._delayed_effects)
361 | self.__update_list(self.bomb_list)
362 | self.__update_list(self.fire_list)
363 | self.__update_list(self.ammunition_list)
364 | self.__update_list(self.value_block_list)
365 | self.__update_list(self.players.values())
366 |
367 | # Turn not alive player into dead bodies:
368 | for pid, player in self.players.items():
369 | if not player.is_alive:
370 | self.dead_player_list.append(self._DeadBody(pid, player.pos))
371 |
372 | # Convert expired bomb into fire
373 | for p in self.bomb_list:
374 | if not p.is_alive: # Start a fire
375 | self._start_fire(p.owner_id, p.pos, p.power)
376 |
377 | # Apply delayed effects
378 | for p in self._delayed_effects:
379 | if not p.is_alive: # Apply delayed effect
380 | self._apply_effect(p.effect)
381 |
382 | # Remove expired entiries
383 | self._delayed_effects = self._only_alive(self._delayed_effects)
384 |
385 | self.ammunition_list = self._only_alive(self.ammunition_list)
386 | self.treasure_list = self._only_alive(self.treasure_list)
387 | self.bomb_list = self._only_alive(self.bomb_list)
388 | self.fire_list = self._only_alive(self.fire_list)
389 | self.value_block_list = self._only_alive(self.value_block_list)
390 | #self.players = dict(filter(lambda p: p.is_alive, self.players.values()))
391 |
392 | # Evaluate game termination rules
393 | if not self.is_over:
394 | over_iter_limit = True if self.max_iterations and self.tick_counter > self.max_iterations else False
395 | # There are opponents, if there are more then 1 player still alive
396 | has_opponents = sum(p.is_alive for p in self.players.values()) > 1
397 |
398 | # Game is over when there is at most 1 player left or
399 | # Time limit (number of iterations) exceeded
400 | self.is_over = not has_opponents or over_iter_limit # There can be only one!
401 |
402 | if self.is_over:
403 | # Picking winners: last player standing or highest scoring corps
404 | high_scores = sorted(self.players.items(), key=lambda pid_player: pid_player[1].reward)
405 | score_range = high_scores[-1][1].reward - high_scores[0][1].reward
406 | if has_opponents: # TODO: Tie only possible if the range of scores is 0
407 | self.winner = high_scores[-1] if score_range != 0 else None
408 | else:
409 | self.winner = next(((pid,p) for pid,p in self.players.items() if p.is_alive), None)
410 |
411 | game_state = self._serialize_state()
412 |
413 | # Update agents view of the world
414 | for pid, agent in self._agents.items():
415 | self._update_agent(dt, pid, agent, game_state)
416 |
417 | self.tick_counter += 1
418 |
419 |
420 | def _player_stat(self, pid, player) -> PlayerStat:
421 | return PlayerStat(
422 | name=player.name,
423 | is_bot=self.is_bot(pid),
424 | score=player.reward,
425 | hp=player.hp,
426 | ammo=player.ammo,
427 | position=player.pos
428 | )
429 |
430 | @property
431 | def stats(self) -> GameStats:
432 | return GameStats(
433 | is_over=self.is_over,
434 | iteration=self.tick_counter,
435 | winner_pid=self.winner[0] if self.winner else None,
436 | players={ k: self._player_stat(k, p) for k, p in self.players.items()}
437 | )
438 |
439 |
440 | @property
441 | def all_blocks(self):
442 | return self.static_block_list + self.value_block_list
443 |
444 | @property
445 | def all_entities(self):
446 | # Combine all alive entities into a single list for quereing by the render engine
447 | return \
448 | self.static_block_list + \
449 | self.ammunition_list + \
450 | self.treasure_list + \
451 | self.bomb_list + \
452 | self.fire_list + \
453 | self.value_block_list
454 |
455 |
456 | def _reset_state(self):
457 | self.is_over = False
458 | self.winner = None
459 | self.tick_counter = 0
460 |
461 | # Recet actions queues
462 | self._action_queue:Dict[PID, List[PlayerActions]] = defaultdict(lambda: [])
463 | self._delayed_effects:List[Game._DelayedEffect] = []
464 |
465 | self.static_block_list:List[Game._IndestructibleBlock] = []
466 |
467 | self.ammunition_list:List[Game._Ammunitation] = []
468 | self.treasure_list:List[Game._Treasure] = []
469 | self.bomb_list:List[Game._Bomb] = []
470 | self.fire_list:List[Game._Fire] = []
471 | self.value_block_list:List[Game._Destructable] = []
472 | self.dead_player_list:List[Game._DeadBody] = []
473 |
474 | for player in self.players.values():
475 | player.ammo = self.PLAYER_START_AMMO
476 | player.power = self.PLAYER_START_POWER
477 | player.reward = 0
478 | player._ttl = self.PLAYER_START_HP
479 |
480 |
481 | def generate_map(self, seed=1):
482 | self._reset_state()
483 |
484 | # FIXME: We need to record enqueued delayed effects, otherwise replay won't match
485 | self._enqueue_effect(DelayedEffectType.SPAWN_TREASURE, ttl=random.randint(self.TREASURE_SPAWN_FREQUENCY_MIN, self.TREASURE_SPAWN_FREQUENCY_MAX))
486 |
487 | all_cells = []
488 | for x in range(0, self.column_count):
489 | for y in range(0, self.row_count):
490 | all_cells.append((x,y))
491 |
492 | # Place players
493 | for player in self.players.values():
494 | player.pos = random.choice(all_cells)
495 | all_cells.remove(player.pos)
496 | # Make sure there are at least 5 free cells around a player spawning position
497 | x, y = player.pos
498 | cross = [ (x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1) ]
499 | extras = [ (x - 2, y), (x + 2, y), (x, y - 2), (x, y + 2) ]
500 | for c in cross:
501 | if c in all_cells:
502 | all_cells.remove(c)
503 | while extras:
504 | e_id = random.choice(range(0, len(extras)))
505 | e = extras.pop(e_id)
506 | if e in all_cells:
507 | all_cells.remove(e)
508 | break
509 |
510 | static_blocks = random.sample(all_cells, self.STATIC_BLOCK_COUNT)
511 | for cell in static_blocks:
512 | self.static_block_list.append(self._IndestructibleBlock(cell))
513 | all_cells.remove(cell)
514 |
515 | soft_blocks = random.sample(all_cells, self.SOFT_BLOCK_COUNT)
516 | for cell in soft_blocks:
517 | self.value_block_list.append(self._SoftBlock(cell, self.SOFTBLOCK_HP))
518 | all_cells.remove(cell)
519 |
520 | ore_blocks = random.sample(all_cells, self.ORE_BLOCK_COUNT)
521 | for cell in ore_blocks:
522 | self.value_block_list.append(self._OreBlock(cell, self.ORE_BLOCK_HP))
523 | all_cells.remove(cell)
524 |
525 | free_ammo = random.sample(all_cells, self.FREE_AMMO_COUNT)
526 | for cell in free_ammo:
527 | self.ammunition_list.append(self._Ammunitation(cell, ttl=self.AMMO_PERISH_TTL, on_perish=lambda: self._enqueue_effect(DelayedEffectType.SPAWN_AMMO, ttl=self.AMMO_RESPAWN_TTL)))
528 | all_cells.remove(cell)
529 |
530 | self.recorder.record(self.tick_counter, GameSysAction(GameSysActions.MAP, self._serialize_map()))
531 |
532 | def _serialize_state(self) -> GameState:
533 | return GameState(
534 | is_over=self.is_over,
535 | tick_number=self.tick_counter,
536 | size=(self.column_count, self.row_count),
537 |
538 | game_map=self._serialize_map(),
539 | ammo=[a.pos for a in self.ammunition_list].copy(),
540 | treasure=[a.pos for a in self.treasure_list].copy(),
541 | bombs=[a.pos for a in self.bomb_list].copy(),
542 | blocks=[(block.Tag, block.pos) for block in self.all_blocks].copy(),
543 | players=[(pid, player.pos) for pid, player in self.players.items()].copy(),
544 | )
545 |
546 | def _serialize_map(self):
547 | # Build an occupancy map for AI-agents to base decisions on
548 | game_map ={}
549 |
550 | def __set_tag(pos, tag):
551 | game_map.setdefault(pos[0], {})
552 | game_map[pos[0]][pos[1]] = tag
553 |
554 | for pid, player in self.players.items():
555 | __set_tag(player.pos, pid)
556 |
557 | for item in self.static_block_list:
558 | __set_tag(item.pos, item.Tag)
559 |
560 | for item in self.value_block_list:
561 | __set_tag(item.pos, item.Tag)
562 |
563 | for item in self.ammunition_list:
564 | __set_tag(item.pos, item.Tag)
565 |
566 | for item in self.treasure_list:
567 | __set_tag(item.pos, item.Tag)
568 |
569 | for item in self.bomb_list:
570 | __set_tag(item.pos, item.Tag)
571 |
572 | return game_map
573 |
574 | def _is_in_bounds(self, p:Point):
575 | return p[0] >= 0 and p[0] < self.column_count and \
576 | p[1] >= 0 and p[1] < self.row_count
577 |
578 | def _collision_list(self, pos, items_list):
579 | return [p for p in items_list if collide(pos, p.pos)]
580 |
581 | def _next_pid(self) -> PID:
582 | pid, self._pid_counter = self._pid_counter, self._pid_counter + 1
583 | return pid
584 |
585 | def _enqueue_effect(self, effect: DelayedEffectType, ttl:int):
586 | if not effect or ttl <= 0:
587 | return
588 |
589 | self._delayed_effects.append(self._DelayedEffect(effect=effect, ttl=ttl))
590 |
591 |
592 | def _apply_action(self, pid: PID, action: PlayerActions) -> bool:
593 | self.recorder.record(self.tick_counter, PlayerMove(pid=pid, action=action))
594 |
595 | if not action:
596 | return False
597 |
598 | player = self.players[pid] if pid in self.players else None
599 | if not player or not player.is_alive: # Dead or non-existing players can't move, obsly
600 | return False
601 |
602 | if action == PlayerActions.NO_OP: return True
603 | elif action == PlayerActions.MOVE_UP: return self._move(pid, player, (0, +1))
604 | elif action == PlayerActions.MOVE_DOWN: return self._move(pid, player, (0, -1))
605 | elif action == PlayerActions.MOVE_LEFT: return self._move(pid, player, (-1, 0))
606 | elif action == PlayerActions.MOVE_RIGHT: return self._move(pid, player, (+1, 0))
607 | elif action == PlayerActions.PLACE_BOMB: return self._place_bomb(pid, player)
608 | else:
609 | logger.error(f"Attempt to apply unknown action: '{action}'")
610 | # TODO: Record cheeting attempt
611 | return False
612 |
613 | def _apply_effect(self, effect: DelayedEffectType):
614 | if not effect: return
615 |
616 | if effect == DelayedEffectType.SPAWN_AMMO: self._spawn_ammo()
617 | elif effect == DelayedEffectType.SPAWN_TREASURE: self._spawn_treasure()
618 | else:
619 | logger.error(f"Attempt to apply unknown effect: '{effect}'")
620 | # TODO: Record cheeting attempt
621 |
622 | def __update_list(self, lt):
623 | for i in lt:
624 | i.update()
625 |
626 | def _only_alive(self, items):
627 | return [i for i in items if i.is_alive]
628 |
629 | def _alive_players(self) -> List[Tuple[PID, _Player]]:
630 | return [(pid,p) for pid,p in self.players.items() if p.is_alive]
631 |
632 | def _get_agent_input(self, pid, agent):
633 | player = self.players[pid] if pid in self.players else None
634 | if not player or not player.is_alive:
635 | return
636 |
637 | logger.debug(f"[{player.name}] agent move....")
638 | chosen_move = agent.next_move()
639 | logger.debug(f"[{player.name}] agent move -> [{chosen_move}]")
640 |
641 | if not chosen_move:
642 | return # NO-OP action
643 |
644 | if chosen_move not in self.ACTION_CODES:
645 | logger.warn(f"Agent for '[{player.name}]' produced unxepected move '{chosen_move}', ignoring")
646 | return
647 |
648 | return self.ACTION_CODES[chosen_move] if chosen_move in self.ACTION_CODES else None
649 |
650 | def _update_agent(self, delta_time: float, pid, agent, game_map):
651 | player = self.players[pid] if pid in self.players else None
652 | if not player or not player.is_alive:
653 | return
654 |
655 | player_state = self._player_state(pid, player)
656 | agent.update(game_map, player_state)
657 |
658 |
659 | def _player_state(self, id:PID, player:_Player):
660 | return PlayerState(id=id, ammo=player.ammo, hp=player.hp, location=player.pos, reward=player.reward, power=player.power) \
661 | if player else None
662 |
663 | def _try_add_fire(self, owner_pid:PID, pos:Point) -> bool:
664 | if not self._is_in_bounds(pos):
665 | return False
666 |
667 | if self._collision_list(pos, self.all_blocks) or self._collision_list(pos, self.bomb_list):
668 | self.fire_list.append(self._Fire(owner_pid, pos))
669 | return False
670 |
671 | self.fire_list.append(self._Fire(owner_pid, pos))
672 | return True
673 |
674 | def _start_fire(self, owner_pid:PID, loc:Point, power:int):
675 | (cell_x, cell_y) = loc
676 |
677 | self.fire_list.append(self._Fire(owner_pid, loc))
678 | for i in range(1, power + 1):
679 | if not self._try_add_fire(owner_pid, (cell_x - i, cell_y)):
680 | break
681 |
682 | for i in range(1, power + 1):
683 | if not self._try_add_fire(owner_pid, (cell_x + i, cell_y)):
684 | break
685 |
686 | for i in range(1, power + 1):
687 | if not self._try_add_fire(owner_pid, (cell_x, cell_y - i)):
688 | break
689 |
690 | for i in range(1, power + 1):
691 | if not self._try_add_fire(owner_pid, (cell_x, cell_y + i)):
692 | break
693 |
694 | def _place_bomb(self, pid: PID, player: _Player) -> bool:
695 | if player.ammo <= 0: # Need to have something to place
696 | return False
697 |
698 | hit_list = self._collision_list(player.pos, self.bomb_list)
699 | if hit_list: # Don't place a bomb on top of a bomb!
700 | return False
701 |
702 | player.ammo -= 1
703 |
704 | self.bomb_list.append(self._Bomb(pid, player.pos, self.BOMB_TTL, player.power))
705 | # TODO Update occupancy greed
706 |
707 | # Schedule respawn of an ammo for the next turn
708 | self._enqueue_effect(DelayedEffectType.SPAWN_AMMO, ttl=self.AMMO_RESPAWN_TTL)
709 |
710 | return True
711 |
712 | def _move(self, pid: PID, player: _Player, delta) -> bool:
713 | new_loc = (max(0, min(player.pos[0] + delta[0], self.column_count - 1)), max(0, min(player.pos[1] + delta[1], self.row_count-1)))
714 | if self._has_collision(new_loc):
715 | return False
716 |
717 | player.pos = new_loc
718 |
719 | return True
720 |
721 | def _pick_good_spots(self):
722 | all_cells = []
723 |
724 | def __safe_remove(pos):
725 | try:
726 | all_cells.remove(pos)
727 | except:
728 | pass
729 |
730 | for x in range(0, self.column_count):
731 | for y in range(0, self.row_count):
732 | all_cells.append((x,y))
733 |
734 | # Don't spawn ammo on top of a player
735 | for player in self.players.values():
736 | __safe_remove(player.pos)
737 |
738 | # Don't spawn ammo on top of a block
739 | for block in self.all_blocks:
740 | __safe_remove(block.pos)
741 |
742 | # Don't spawn ammo on top of another ammo
743 | for ammo in self.ammunition_list:
744 | __safe_remove(ammo.pos)
745 |
746 | # Don't spawn ammo on top of treasure ammo
747 | for ammo in self.treasure_list:
748 | __safe_remove(ammo.pos)
749 |
750 | # Don't spawn ammo on top of a bomb
751 | for ammo in self.bomb_list:
752 | __safe_remove(ammo.pos)
753 |
754 | return all_cells
755 |
756 | def _spawn_treasure(self):
757 | good_locations = self._pick_good_spots()
758 | if not good_locations:
759 | self._enqueue_effect(DelayedEffectType.SPAWN_TREASURE, ttl=random.randint(1, self.TREASURE_SPAWN_FREQUENCY_MIN))
760 | return False
761 |
762 | loc = random.choice(good_locations)
763 | self.treasure_list.append(Game._Treasure(loc))
764 | self._enqueue_effect(DelayedEffectType.SPAWN_TREASURE, ttl=random.randint(self.TREASURE_SPAWN_FREQUENCY_MIN, self.TREASURE_SPAWN_FREQUENCY_MAX))
765 |
766 | return True
767 |
768 | def _spawn_ammo(self):
769 | good_locations = self._pick_good_spots()
770 | if not good_locations:
771 | self._enqueue_effect(DelayedEffectType.SPAWN_AMMO, ttl=self.AMMO_RESPAWN_TTL)
772 | return False
773 |
774 | loc = random.choice(good_locations)
775 | self.ammunition_list.append(self._Ammunitation(loc, ttl=self.AMMO_PERISH_TTL, on_perish=lambda: self._enqueue_effect(DelayedEffectType.SPAWN_AMMO, ttl=self.AMMO_RESPAWN_TTL)))
776 |
777 | return True
778 |
779 | def _has_collision(self, pos:Point) -> bool:
780 | """ It checks if a player can move to a new location"""
781 | # Check if given postion overlaps with any of the blocks
782 | hit_list = self._collision_list(pos, self.all_blocks)
783 |
784 | # Check if given postion overlaps with any of the players
785 | # This makes players non-clipable
786 | hit_list += self._collision_list(pos, self.players.values())
787 |
788 | # Make bomb non-clipable
789 | hit_list += self._collision_list(pos, self.bomb_list)
790 |
791 | # Loop through each colliding sprite, remove it, and add to the score.
792 | return len(hit_list) > 0
793 |
794 |
--------------------------------------------------------------------------------
/coderone/dungeon/game_recorder.py:
--------------------------------------------------------------------------------
1 | import jsonplus
2 | from .game import Recorder, GameEvent, GameSysAction, PlayerMove
3 |
4 | class FileRecorder(Recorder):
5 | """ A game recording that saves the game into a file
6 | """
7 | def __init__(self, file_name:str):
8 | self.file = open(file_name, mode='wt')
9 |
10 | def __enter__(self):
11 | return self
12 |
13 | def __exit__(self, exc_type, exc_value, traceback):
14 | if self.file:
15 | self.file.close()
16 | self.file = None
17 |
18 | def record(self, tick:int, event: GameEvent):
19 | self.file.write(f"{tick}: ")
20 |
21 | if isinstance(event, GameSysAction):
22 | self.file.write(f"{event.action.value} ")
23 | self.file.write(jsonplus.dumps(event.payload))
24 |
25 | elif isinstance(event, PlayerMove):
26 | self.file.write(f"{event.pid} {event.action.value}")
27 |
28 | self.file.write("\n")
29 | self.file.flush()
30 |
31 |
--------------------------------------------------------------------------------
/coderone/dungeon/hack_client.py:
--------------------------------------------------------------------------------
1 | import time
2 | import logging
3 |
4 | import curses
5 | from curses import wrapper
6 | from curses.textpad import Textbox, rectangle
7 |
8 | from .game import PlayerActions, Point, PID, Game
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class Client:
14 | def __init__(self, width:int, height:int, title:str, game=None, config=None, interactive:bool=False, user_pid:PID=None):
15 | self.title = title
16 | self.game = game
17 | self.config = config
18 | self.interactive = interactive
19 |
20 | def _update(self, tick_step):
21 | self.game.tick(tick_step)
22 |
23 | stats = self.game.stats()
24 | for p in stats['players'].values():
25 | name = "{}{}".format(p['name'], '(bot)' if p['is_bot'] else "")
26 | logger.info(f"{name} HP: {p['hp']} / Ammo: {p['ammo']} / Score: {p['score']}, loc: ({p['position'][0]}, {p['position'][0]})")
27 |
28 | def run(self, tick_step):
29 | wrapper(lambda src: self.main(src, tick_step))
30 |
31 | def main(self, stdscr, tick_step):
32 | stdscr.addstr(0, 0, self.title)
33 |
34 | editwin = curses.newwin(5,30, 2,1)
35 | rectangle(stdscr, 1,0, 1+5+1, 1+30+1)
36 | stdscr.refresh()
37 |
38 | box = Textbox(editwin)
39 |
40 | # Let the user edit until Ctrl-G is struck.
41 | box.edit()
42 |
43 | # Get resulting contents
44 | message = box.gather()
45 |
46 | def _run(self, tick_step):
47 | try:
48 | while not self.game.is_over:
49 | logger.info(f"Game step [{self.game.tick_counter}/{self.game.max_iterations}]... ")
50 |
51 | cycle_start_time = time.time()
52 | self._update(tick_step)
53 | dt = time.time() - cycle_start_time
54 | logger.debug(f"...step [{self.game.tick_counter}/{self.game.max_iterations}] completed in {dt*1000.0:.4f}ms")
55 |
56 | sleep_time = tick_step - dt
57 | if sleep_time > 0:
58 | logger.debug(f"has time to sleep for {sleep_time}sec")
59 | time.sleep(sleep_time)
60 |
61 | except KeyboardInterrupt:
62 | logger.info(f"user interrupted the game")
63 | pass
64 |
--------------------------------------------------------------------------------
/coderone/dungeon/headless_client.py:
--------------------------------------------------------------------------------
1 | import time
2 | import logging
3 |
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 | class Client:
8 | def __init__(self, game, config):
9 | self.game = game
10 | self.config = config
11 |
12 | self.is_endless = self.config.get('endless', False)
13 | self.paused = False # self.config.get('start_paused', False)
14 | self.single_step = False # self.config.get('single_step', False)
15 |
16 | def _update(self, tick_step):
17 | self.game.tick(tick_step)
18 |
19 | stats = self.game.stats
20 | for p in stats.players.values():
21 | name = "{}{}".format(p.name, '(bot)' if p.is_bot else "")
22 | logger.info(f"{name} HP: {p.hp} / Ammo: {p.ammo} / Score: {p.score}, loc: ({p.position[0]}, {p.position[1]})")
23 |
24 | if self.game.is_over and self.is_endless:
25 | self._reset_game()
26 |
27 | def run(self, tick_step):
28 | try:
29 | while not self.game.is_over:
30 | logger.info(f"game-step [{self.game.tick_counter}/{self.game.max_iterations}]")
31 |
32 | cycle_start_time = time.time()
33 | self._update(tick_step)
34 | dt = time.time() - cycle_start_time
35 | logger.debug(f"game-step [{self.game.tick_counter}/{self.game.max_iterations}] completed in {dt*1000.0:.4f}ms")
36 |
37 | sleep_time = tick_step - dt
38 | if sleep_time > 0:
39 | logger.debug(f"has time to sleep for {sleep_time}sec")
40 | time.sleep(sleep_time)
41 |
42 | except KeyboardInterrupt:
43 | logger.info(f"user interrupted the game")
44 | pass
45 |
46 | def _reset_game(self):
47 | self.game.generate_map()
48 |
--------------------------------------------------------------------------------
/coderone/dungeon/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Dungeons & Data Structures
4 | Coder One AI game tournament challenge
5 | """
6 |
7 | import argparse
8 | import os
9 | import sys
10 | import time
11 | import logging
12 | import jsonplus
13 | from contextlib import ExitStack
14 | from typing import Dict, List, Tuple, Union, NamedTuple, Any, Optional
15 |
16 | from appdirs import user_config_dir
17 |
18 | from .game_recorder import FileRecorder, Recorder
19 | # from coderone.dungeon.agent_driver.simple_driver import Driver, AgentProxy
20 | from .agent_driver.multiproc_driver import Driver, AgentProxy
21 |
22 | from .game import Game
23 |
24 | APP_NAME = 'coderone.dungeon'
25 |
26 | ASSET_DIRECTORY = os.path.join(os.path.dirname(__file__), 'assets')
27 | DEFAULT_CONFIG_FILE = 'config.json'
28 |
29 |
30 | SCREEN_TITLE = "Coder One: Dungeons & Data Structures"
31 |
32 | AGENT_READY_WAIT_TIMEOUT = 3
33 | AGENT_READY_WAIT_SEC = 1 # Max numbre of sec to wait for agent to become ready.
34 | TICK_STEP = 0.1 # Number of seconds per 1 iteration of game loop
35 | ITERATION_LIMIT = 180*10 # Max number of iteration the game should go on for, None for unlimited
36 |
37 | logger = logging.getLogger()
38 | logging.basicConfig(stream=sys.stdout, level=logging.INFO)
39 | # logger.setLevel(logging.DEBUG)
40 |
41 |
42 | def __load_or_generate_config(config_file:Optional[str]) -> dict:
43 | ## Setting up the players using the config file
44 |
45 | if config_file:
46 | # A custom config file location given:
47 | try:
48 | with open(config_file) as f:
49 | config_data = jsonplus.loads(f.read())
50 | except: # Failed to load config, fallback to default values
51 | logger.error(f"config file '{config_file}' not found, using default value")
52 | raise
53 | else:
54 | # Default config file expected:
55 | config_dir = user_config_dir(APP_NAME)
56 | config_file = os.path.join(config_dir, DEFAULT_CONFIG_FILE)
57 |
58 | try:
59 | with open(config_file) as f:
60 | config_data = jsonplus.loads(f.read())
61 | except FileNotFoundError: # Failed to load config, fallback to default values
62 | logger.warning(f"No default config file found, generating...")
63 | config_data = {
64 | "headless": False,
65 | "interactive": False,
66 | "start_paused": True,
67 | "wait_end": 5,
68 | "max_iterations": ITERATION_LIMIT,
69 | "tick_step": TICK_STEP
70 | }
71 |
72 | os.makedirs(config_dir, exist_ok=True)
73 | logger.warning(f"Writing default config into: {config_file}")
74 | with open(config_file, "w") as f:
75 | f.write(jsonplus.pretty(config_data))
76 |
77 |
78 | config_data.setdefault('start_paused', False)
79 | config_data.setdefault('wait_end', 10)
80 | config_data.setdefault('assets', ASSET_DIRECTORY)
81 | config_data.setdefault('interactive', False)
82 | config_data.setdefault('tick_step', TICK_STEP)
83 | config_data.setdefault('no_text', False) # A work around Pillow (Python image library) bug
84 | config_data.setdefault('single_step', False)
85 | config_data.setdefault('endless', False)
86 |
87 | config_data.setdefault('rows', Game.ROW_COUNT)
88 | config_data.setdefault('columns', Game.COLUMN_COUNT)
89 | config_data.setdefault('max_iterations', ITERATION_LIMIT)
90 |
91 | return config_data
92 |
93 |
94 | # Borrowed from flask!
95 | def _prepare_import(path):
96 | """Given a filename this will try to calculate the python path, add it
97 | to the search path and return the actual module name that is expected.
98 | """
99 | path = os.path.realpath(path)
100 | fname, ext = os.path.splitext(path)
101 | if ext == ".py":
102 | path = fname
103 |
104 | if os.path.basename(path) == "__init__":
105 | path = os.path.dirname(path)
106 |
107 | module_name = []
108 |
109 | # move up until outside package structure (no __init__.py)
110 | while True:
111 | path, name = os.path.split(path)
112 | module_name.append(name)
113 |
114 | if not os.path.exists(os.path.join(path, "__init__.py")):
115 | break
116 |
117 | if sys.path[0] != path:
118 | sys.path.insert(0, path)
119 |
120 | return ".".join(module_name[::-1])
121 |
122 |
123 | def __load_agent_drivers(cntx: ExitStack, agent_modules, config:dict, watch=False) -> List[Driver]:
124 | agents = []
125 | n_agents = len(agent_modules)
126 |
127 | logger.info(f"Loading agent modules: {n_agents} required")
128 | for counter, agent_module in enumerate(agent_modules):
129 | try:
130 | logger.info(f"[{counter + 1}/{n_agents}] loading agent driver: {agent_module}")
131 | module_name = _prepare_import(agent_module)
132 | driver = Driver(module_name, watch, config)
133 | cntx.enter_context(driver)
134 | agents.append(driver)
135 | except Exception as e:
136 | logger.error(f"failed to load agent module {agent_module}")
137 | logger.error(e, exc_info=True)
138 | return None
139 |
140 | return agents
141 |
142 | class TooManyPlayers(Exception):
143 | pass
144 |
145 |
146 | def run(agent_modules, player_names, config=None, recorder=None, watch=False):
147 | # Create a new game
148 | row_count = config.get('rows')
149 | column_count = config.get('columns')
150 | iteration_limit = config.get('max_iterations')
151 | is_interactive = config.get('interactive')
152 |
153 | # Check max number of players support by the map:
154 | squers_per_player = 6
155 | max_players = row_count*column_count / squers_per_player
156 | if max_players < len(agent_modules):
157 | raise TooManyPlayers(f"Game map ({column_count}x{row_count}) supports at most {max_players} players while {len(agent_modules)} agent requested.")
158 |
159 | # Load agent modules
160 | with ExitStack() as stack:
161 | agent_drivers = __load_agent_drivers(stack, agent_modules, watch=watch, config=config)
162 | if not agent_drivers:
163 | return None # Exiting with an error, no contest
164 |
165 | game = Game(row_count=row_count, column_count=column_count, max_iterations=iteration_limit, recorder=recorder)
166 |
167 | # Add all agents to the game
168 | agents: List[AgentProxy] = []
169 | names_len = len(player_names) if player_names else 0
170 | for i, agent_driver in enumerate(agent_drivers):
171 | agent = agent_driver.agent()
172 | agents.append(agent)
173 | game.add_agent(agent, player_names[i] if i < names_len else agent_driver.name)
174 |
175 | # Add a player for the user if running in interactive mode or configured interactive
176 | user_pid = game.add_player("Player") if is_interactive else None
177 | game.generate_map()
178 |
179 | wait_time = AGENT_READY_WAIT_TIMEOUT
180 | time.sleep(0.1) # Yeld to sub-processes a chance to start and initialise agents
181 | agents_not_ready = [a.name for a in agents if not a.is_ready]
182 | while agents_not_ready and wait_time > 0:
183 | logger.info(f"Waiting for slowpoke agents [{wait_time} sec]: {agents_not_ready}")
184 | time.sleep(AGENT_READY_WAIT_SEC)
185 | wait_time -= AGENT_READY_WAIT_SEC
186 | agents_not_ready = [a.name for a in agents if not a.is_ready]
187 |
188 | if agents_not_ready:
189 | logger.info(f"Agents {agents_not_ready} are still not ready even after {AGENT_READY_WAIT_TIMEOUT}sec. Starting the match anyways")
190 |
191 | tick_step = config.get('tick_step')
192 | if config.get('headless'):
193 | from .headless_client import Client
194 |
195 | client = Client(game=game, config=config)
196 | client.run(tick_step)
197 | else:
198 | if config.get('hack'):
199 | from .hack_client import Client
200 | screen_width = 80
201 | screen_height = 24
202 | else:
203 | from .arcade_client import Client, WIDTH, HEIGHT, PADDING
204 |
205 | screen_width = PADDING[0]*2 + WIDTH * 12
206 | screen_height = PADDING[1]*3 + HEIGHT * 10
207 |
208 | window = Client(width=screen_width, height=screen_height, title=SCREEN_TITLE, game=game, config=config, interactive=is_interactive, user_pid=user_pid)
209 | window.run(tick_step)
210 |
211 | # Announce game winner and exit
212 | return game.stats
213 |
214 |
215 | def run_match(agents:List[str], players:List[str]=None, config_name:str=None, record_file:str=None, watch:bool=False, args:Any=None):
216 | config = __load_or_generate_config(config_name)
217 | if args:
218 | if args.headless or 'headless' not in config: config['headless'] = args.headless
219 | if args.interactive or 'interactive' not in config: config['interactive'] = args.interactive
220 | if args.hack or 'hack' not in config: config['hack'] = args.hack
221 | if args.no_text or 'no_text' not in config: config['no_text'] = args.no_text
222 | if args.start_paused or 'start_paused' not in config: config['start_paused'] = args.start_paused
223 | if args.single_step or 'single_step' not in config: config['single_step'] = args.single_step
224 | if args.endless or 'endless' not in config: config['endless'] = args.endless
225 |
226 | # if args.watch or 'watch' not in config: config['watch'] = args.watch
227 | # if args.record or 'record' not in config: config['record'] = args.record
228 | # if args.wait_end or 'wait_end' not in config: config['wait_end'] = args.wait_end
229 | # if args.tick_step or 'tick_step' not in config: config['tick_step'] = args.tick_step
230 |
231 | recorder = FileRecorder(record_file) if record_file else Recorder()
232 |
233 | # Everything seems in order - lets start the game
234 | with recorder:
235 | return run(agent_modules=agents, player_names=players, config=config, recorder=recorder, watch=watch)
236 |
237 |
238 | def submit_agent(agent_module:str):
239 | """ Submit agent module for the team entry into the tournament.
240 | """
241 | path = os.path.realpath(agent_module)
242 | fname, ext = os.path.splitext(path)
243 | if ext == ".py": # A single file with .py extention specified
244 | module_name = os.path.basename(fname)
245 | single = True
246 | if not os.path.exists(path):
247 | print(f"Error: specfied file not found '{agent_module}'\n"
248 | "No files submitted.", file=sys.stderr)
249 | return
250 | elif not ext and os.path.exists(f'{path}.py'):
251 | module_name = os.path.basename(fname)
252 | path = f'{path}.py'
253 | single = True
254 | else:
255 | module_name = agent_module
256 | single = False
257 | if not os.path.exists(path):
258 | print(f"Error: directory found for the specified module: '{agent_module}'\n"
259 | "No files submitted.", file=sys.stderr)
260 | return
261 |
262 | # Make sure there is a valid __init__.py if its a module:
263 | if not single:
264 | if not os.path.exists(os.path.join(path, '__init__.py')):
265 | print(f"Error, specfied location '{agent_module}' is a directory, but does not appear to be a properly-formed python module.\n"
266 | "Check the path or add missing '__init__.py' file.\n"
267 | "No files submitted.",
268 | file=sys.stderr)
269 | return
270 |
271 | from .publisher import submit
272 | try:
273 | submit(agent_module=module_name, single=single, source_file=path)
274 | except KeyboardInterrupt:
275 | # print("Canceled.")
276 | return
277 |
278 |
279 | def main():
280 | parser = argparse.ArgumentParser(description=SCREEN_TITLE)
281 |
282 | parser.add_argument('--headless', action='store_true',
283 | default=False,
284 | help='run without graphics')
285 | parser.add_argument('--interactive', action='store_true',
286 | default=False,
287 | help='all a user to contol a player')
288 | parser.add_argument('--no_text', action='store_true',
289 | default=False,
290 | help='Graphics bug workaround - disables all text')
291 | parser.add_argument('--players', type=str,
292 | help="Comma-separated list of player names")
293 | parser.add_argument('--hack', action='store_true',
294 | default=False,
295 | help=argparse.SUPPRESS)
296 | parser.add_argument('--start_paused', action='store_true',
297 | default=False,
298 | help='Start a game in pause mode, only if interactive')
299 | parser.add_argument('--single_step', action='store_true',
300 | default=False,
301 | help='Game will run one step at a time awaiting for player input')
302 | parser.add_argument('--endless', action='store_true',
303 | default=False,
304 | help='Game will restart after the match is over. indefinitely')
305 |
306 | parser.add_argument('--submit', action='store_true',
307 | default=False,
308 | help="Don't run the game, but submit the agent as team entry into the trournament")
309 |
310 | parser.add_argument('--record', type=str,
311 | help='file name to record game')
312 | parser.add_argument('--watch', action='store_true',
313 | default=False,
314 | help='automatically reload agents on file changes')
315 | parser.add_argument('--config', type=str,
316 | default=None,
317 | help='path to the custom config file')
318 |
319 | parser.add_argument("agents", nargs="+", help="agent module")
320 |
321 | args = parser.parse_args()
322 |
323 | n_agents = len(args.agents)
324 | if args.submit:
325 | if n_agents > 1:
326 | print(
327 | "Error: Only a single agent entry per team is allowed.\n"
328 | f"You have specified {n_agents} agent modules.\n"
329 | "Please chose only one you wish submit and try again.\n"
330 | , file=sys.stderr)
331 | sys.exit(1)
332 |
333 | submit_agent(agent_module=args.agents[0])
334 | sys.exit(0)
335 |
336 | if n_agents < 2 and (args.headless or not args.interactive):
337 | print("At least 2 agents must be provided in the match mode. Exiting", file=sys.stderr)
338 | sys.exit(1)
339 |
340 | if args.headless and args.interactive:
341 | print("Interactive play is not support in headless mode. Exiting", file=sys.stderr)
342 | sys.exit(1)
343 | # TODO: Do we need an error message for 'single_step' if running headless?
344 | if args.headless and args.no_text:
345 | print("Makes no sense to run headless and ask for no-text. Ignoring", file=sys.stderr)
346 | if not args.interactive and args.start_paused:
347 | print("Can not start paused in non-interactive mode. Exiting", file=sys.stderr)
348 | sys.exit(1)
349 |
350 | jsonplus.prefer_compat()
351 |
352 | players = args.players.split(',') if args.players else None
353 |
354 | try:
355 | result = run_match(agents=args.agents, players=players, config_name=args.config, record_file=args.record, watch=args.watch, args=args)
356 | print(jsonplus.pretty(result))
357 | except TooManyPlayers as ex:
358 | print(f'Too many players for the game.\n{ex}', file=sys.stderr)
359 | sys.exit(1)
360 |
361 |
362 | # We done here, all good.
363 | sys.exit(0)
364 |
365 |
366 | if __name__ == "__main__":
367 | main()
368 |
--------------------------------------------------------------------------------
/coderone/dungeon/publisher.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List, Tuple, Union, NamedTuple, Any, Optional
2 | import getpass
3 | import tarfile
4 | import tempfile
5 | import os
6 | import sys
7 |
8 | import jsonplus
9 | import requests
10 |
11 |
12 | BASE_URL = 'https://us-central1-psyched-equator-297906.cloudfunctions.net'
13 | # BASE_URL = 'http://localhost:5001/psyched-equator-297906/us-central1'
14 |
15 |
16 | exclude_names = [
17 | "__pycache__",
18 | "build",
19 | "develop-eggs",
20 | "dist",
21 | "downloads",
22 | "eggs",
23 | ".eggs",
24 | "lib",
25 | "lib64",
26 | "parts"
27 | "sdist",
28 | "var",
29 | "wheels",
30 | "python-wheels",
31 | ".egg-info",
32 | ".pybuilder",
33 | "target",
34 | ".ipynb_checkpoints",
35 | "__pypackages__",
36 |
37 | ".env",
38 | ".venv",
39 | "env",
40 | "venv",
41 | "ENV",
42 | ".pyre",
43 | ".pytype",
44 | "cython_debug"
45 | ]
46 |
47 | exclude_ext = [
48 | ".tmp",
49 | ".pyc", ".pyo", ".pyd",
50 | ".class",
51 | ".so",
52 | ".egg-info",
53 | ".egg",
54 | ".manifest",
55 | ".spec",
56 | ".cover",
57 | ".log",
58 | ]
59 |
60 |
61 | def filter_function(tarinfo):
62 | if tarinfo.name in exclude_names:
63 | return None
64 |
65 | if os.path.splitext(tarinfo.name)[1] in exclude_names:
66 | return None
67 |
68 | return tarinfo
69 |
70 |
71 | class AuthInfo(NamedTuple):
72 | user_email:str
73 | token:str
74 | team_id:str
75 | team_name:str
76 |
77 | def _yes_or_no(question):
78 | while "the answer is invalid":
79 | reply = str(input(question + ' (Y/n): ')).lower().strip()
80 | if not reply or reply[0] == 'y':
81 | return True
82 | if reply[0] == 'n':
83 | return False
84 |
85 |
86 | class AuthError(Exception):
87 | pass
88 |
89 |
90 | def _auth_team(user_email, passw) -> AuthInfo:
91 | print(f"Logging as '{user_email}'")
92 |
93 | user_r = requests.post(f'{BASE_URL}/auth',
94 | data = {"email":user_email,"password":passw,"returnSecureToken": True}
95 | )
96 |
97 | response_json = user_r.json()
98 | if 'idToken' not in response_json:
99 | raise AuthError('Unauthorised' if 'error' not in response_json else response_json['error']['message'])
100 |
101 | user_token = response_json['idToken']
102 | r = requests.get(f'{BASE_URL}/api/my-team', headers = {"authorization": f"Bearer {user_token}"})
103 | if r.status_code != 200:
104 | try:
105 | r.raise_for_status()
106 | except requests.exceptions.HTTPError:
107 | raise AuthError("Could not find participants team")
108 |
109 | team_info = r.json()
110 | print(team_info)
111 |
112 | team_id = team_info['teamId']
113 | if not team_id:
114 | raise AuthError("Could not find participants team")
115 |
116 | return AuthInfo(
117 | user_email = response_json['email'],
118 | token = user_token,
119 | team_id = team_id,
120 | team_name = team_info['teamName']
121 | )
122 |
123 |
124 | def _submit_agent_code(module_archive, agent_name, single, auth:AuthInfo):
125 | print(f"Uploading agent archive '{agent_name}'")
126 |
127 | r = requests.put(f'{BASE_URL}/api/upload_url/{auth.team_id}/{agent_name}', params={'filename': f'{agent_name}.tar.gz', 'single': single}, headers = {"authorization": f"Bearer {auth.token}"})
128 | if r.status_code == 200:
129 | pass
130 | elif r.status_code == 401:
131 | response_json = r.json()
132 | print(f"❌ {response_json['message']}. Please check team Id and that you are a member of that team", file=sys.stderr)
133 | return
134 | else:
135 | try:
136 | r.raise_for_status()
137 | except requests.exceptions.HTTPError as ex:
138 | print(f"❌ Error uploading submission file: {ex}", file=sys.stderr)
139 | return
140 |
141 | response_json = r.json()
142 | upload_url = response_json['upload_url']
143 |
144 | r = requests.put(upload_url, module_archive, headers = {'Content-Type': 'application/octet-stream'})
145 | if r.status_code == 200:
146 | print('✓ Done.')
147 | else:
148 | try:
149 | r.raise_for_status()
150 | except requests.exceptions.HTTPError as ex:
151 | print(f"❌ Error uploading submission file: {ex}", file=sys.stderr)
152 | return
153 |
154 |
155 | def submit(agent_module:str, single:bool, source_file:str):
156 | """ Submit agent module for the team entry into the tournament.
157 | """
158 | print(f"Submitting your agent '{agent_module}' for the tournament entry.")
159 | user_id = input("User email: ")
160 | passw = input("Submission token: ") # getpass.getpass(prompt='submission token: ')
161 |
162 | try:
163 | auth_info = _auth_team(user_id, passw)
164 | except AuthError as ex:
165 | print(f"❌ Failed: {ex}")
166 | return
167 |
168 | print(f"Submitting {'single-file' if single else 'module'} agent: '{agent_module}' for team '{auth_info.team_name}' by user '{auth_info.user_email}'")
169 |
170 | confirmed = _yes_or_no('Are this details correct?')
171 | if not confirmed:
172 | print("Canceled. No files submitted")
173 | return
174 |
175 | with tempfile.NamedTemporaryFile(suffix='.tar.gz') as f:
176 | with tarfile.open(fileobj=f, mode='w:gz') as tar:
177 | tar.add(source_file, arcname=os.path.basename(source_file), filter=filter_function)
178 |
179 | f.flush()
180 | f.seek(0)
181 |
182 | _submit_agent_code(f, agent_name=agent_module, single=single, auth=auth_info)
183 |
--------------------------------------------------------------------------------
/coderone/dungeon/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy==1.19.3
2 |
3 | jsonplus==0.8.0
4 | appdirs==1.4.4
5 | arcade==2.4.3
6 | watchdog==0.10.4
7 |
8 | requests==2.25.0
9 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "no_text": false,
3 | "headless": false,
4 | "interactive": false,
5 | "start_paused": true,
6 | "wait_end": 5,
7 | "max_iterations": 3000,
8 | "update_time_step1": 0.50
9 | }
--------------------------------------------------------------------------------
/modular_agent/__init__.py:
--------------------------------------------------------------------------------
1 | from . import my_agent
2 |
3 | def agent():
4 | return my_agent.FreeRoamingAgent()
--------------------------------------------------------------------------------
/modular_agent/stand_still.py:
--------------------------------------------------------------------------------
1 | '''
2 | Exaple agent that does not do anything
3 | '''
4 | import time
5 |
6 | class agent:
7 | ACTION_PALLET = ['']
8 |
9 | def __init__(self):
10 | """ Example of a random agent
11 | """
12 | pass
13 |
14 | def next_move(self, ascii_map, state_dic):
15 | """ This method is called each time the agent is required to choose an action
16 | """
17 |
18 | # Lets pretend that agent is doing some thinking
19 | time.sleep(1)
20 |
21 | return self.ACTION_PALLET[0]
22 |
23 |
--------------------------------------------------------------------------------
/random_agent.py:
--------------------------------------------------------------------------------
1 | '''
2 | Example of a random agent
3 | '''
4 | import time
5 | import random
6 |
7 | class agent:
8 | """ Example of a random agent
9 | """
10 |
11 | ACTION_PALLET = ['', 'u','d','l','r','p', '']
12 |
13 | def __init__(self):
14 | self.name = "random bot"
15 | """ Your agent initialization code goes here.
16 | """
17 | raise Exception("BOOM")
18 |
19 | def next_move(self, ascii_map, state_dic):
20 | """ This method is called each time the agent is required to choose an action
21 | """
22 |
23 | return random.choice(self.ACTION_PALLET)
24 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r", encoding="utf-8") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name='coderone-challenge-dungeon',
8 | version='0.1.6',
9 | description='Dungeons and data structures: Coder one AI Game Tournament',
10 | url='https://github.com/gocoderone/dungeons-and-data-structures',
11 | author='Ivan Ryabov',
12 | author_email='HUMANS@gocoder.one',
13 | long_description=long_description,
14 | long_description_content_type="text/markdown",
15 | packages=setuptools.find_packages(),
16 | classifiers=[
17 | "Programming Language :: Python :: 3",
18 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
19 | "Operating System :: OS Independent",
20 | ],
21 | install_requires=[
22 | 'pymunk==5.7.0',
23 | 'appdirs==1.4.4',
24 | 'arcade==2.4.3',
25 | 'watchdog==0.10.4',
26 | 'jsonplus==0.8.0',
27 |
28 | 'requests==2.25.0'
29 | ],
30 | python_requires='>=3.6',
31 | entry_points = {
32 | 'console_scripts': [
33 | 'coderone-dungeon=coderone.dungeon.main:main'
34 | ],
35 | },
36 |
37 | zip_safe=False,
38 | package_data = {
39 | 'coderone.dungeon': [
40 | 'assets/sounds/explosion.mp3',
41 |
42 | 'assets/images/bomb_64px.png',
43 | 'assets/images/ammo.png',
44 | 'assets/images/chest.png',
45 | 'assets/images/crate.png',
46 | 'assets/images/ore_block.png',
47 | 'assets/images/metal_block.png',
48 | 'assets/images/skelet_run_anim_f1.png',
49 | 'assets/images/coin_anim_f0.png',
50 | 'assets/images/explosion.png',
51 |
52 | 'assets/images/floor_1.png',
53 | 'assets/images/floor_2.png',
54 | 'assets/images/floor_3.png',
55 | 'assets/images/floor_4.png',
56 | 'assets/images/floor_5.png',
57 | 'assets/images/floor_6.png',
58 | 'assets/images/floor_7.png',
59 | 'assets/images/floor_8.png',
60 |
61 | 'assets/images/p1_knight_64px.png',
62 | 'assets/images/p2_knight_64px.png',
63 | 'assets/images/p2_knight_64px_flipped.png',
64 | 'assets/images/p2_knight_orange_64px_flipped.png',
65 | 'assets/images/wizard_m_64px.png',
66 | 'assets/images/wizard_f_64px.png',
67 |
68 | "assets/images/chrome/wall_side_top_left.png",
69 | "assets/images/chrome/wall_side_top_right.png",
70 | "assets/images/chrome/wall_side_front_left.png",
71 | "assets/images/chrome/wall_side_front_right.png",
72 |
73 | "assets/images/chrome/wall_corner_top_left.png",
74 | "assets/images/chrome/wall_corner_top_right.png",
75 |
76 | "assets/images/chrome/wall_corner_front_left.png",
77 | "assets/images/chrome/wall_corner_front_right.png",
78 |
79 | "assets/images/chrome/wall_top_left.png",
80 | "assets/images/chrome/wall_top_mid.png",
81 | "assets/images/chrome/wall_top_right.png",
82 | "assets/images/chrome/wall_side_mid_left.png",
83 | "assets/images/chrome/wall_mid.png",
84 | "assets/images/chrome/wall_side_mid_right.png",
85 |
86 | ]
87 | },
88 | )
--------------------------------------------------------------------------------