diff --git a/.gitignore b/.gitignore index 71856bf..3db154c 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,6 @@ cython_debug/ # Mac bs .DS_store + +# Random stuff +__pycache__/ diff --git a/LICENSE b/LICENSE index 91ee689..a612ad9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,373 @@ -MIT License +Mozilla Public License Version 2.0 +================================== -Copyright (c) 2023 Vasilis Valatsos +1. Definitions +-------------- -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index b742941..1f3049b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ -# PneuMARL -A PyGame2 Zelda-like embedded with MARL algorithm. + +# Pneuma: Reinforcement Learning Platform + +## Introduction + +Pneuma is a Reinforcement Learning platform created as part of a thesis project. It is developed using PyGame and offers a customizable environment for testing and implementing reinforcement learning algorithms. + +## Installation + +To install Pneuma, clone this repository and install the requirements (`requirements.txt`) + +After cloning, you can edit the agents, create your own, and modify pneuma.py (the main file). Additionally, consider editing player.setup_agent() for further customization. +Note + +- [] TODO: Separate the update logic from the network logic inside the player. + +## Usage + +To run Pneuma, use the command-line interface with the following options: + +- `--no_seed`: If set to True, runs the program without a seed. Default is False. +- `--seed [int]`: Specifies the seed for the random number generator. Default is 1. +- `--n_episodes [int]`: Defines the number of episodes. Default is 300. +- `--ep_length [int]`: Sets the length of each episode. Default is 5000. +- `--n_players [int]`: Number of players. Default is 1. +- `--chkpt_path [str]`: Path for saving/loading agent models. Default is "agents/saved_models". +- `--figure_path [str]`: Path for saving figures. Default is "figures". +- `--horizon [int]`: Number of steps per update. Default is 200. +- `--show_pg`: If True, opens a PyGame window on the desktop. Default is False. +- `--no_load`: If True, ignores saved models. Default is False. +- `--gamma [float]`: The gamma parameter for PPO. Default is 0.99. +- `--alpha [float]`: The alpha parameter for PPO. Default is 0.0003. +- `--policy_clip [float]`: The policy clip. Default is 0.2. +- `--batch_size [int]`: Size of each batch. Default is 64. +- `--n_epochs [int]`: Number of epochs. Default is 10. +- `--gae_lambda [float]`: The lambda parameter of the GAE. Default is 0.95. + +### Example Command + +```bash +$ python pneuma.py --seed 42 --n_episodes 300 --ep_length 5000 --n_players 2 --no_load +``` + +## License + +Pneuma is licensed under the Mozilla Public License 2.0. \ No newline at end of file diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agents/ppo/agent.py b/agents/ppo/agent.py index 9d76ea7..f287f78 100644 --- a/agents/ppo/agent.py +++ b/agents/ppo/agent.py @@ -8,15 +8,19 @@ class Agent: def __init__(self, input_dims, n_actions, gamma=0.99, alpha=0.0003, policy_clip=0.2, batch_size=64, N=2048, n_epochs=10, - gae_lambda=0.95): + gae_lambda=0.95, chkpt_dir='tmp/ppo'): self.gamma = gamma self.policy_clip = policy_clip self.n_epochs = n_epochs self.gae_lambda = gae_lambda - self.actor = ActorNetwork(input_dims, n_actions, alpha) - self.critic = CriticNetwork(input_dims, alpha) + self.actor = ActorNetwork( + input_dims, n_actions, alpha, chkpt_dir=chkpt_dir) + + self.critic = CriticNetwork( + input_dims, alpha, chkpt_dir=chkpt_dir) + self.memory = PPOMemory(batch_size) def remember(self, state, action, probs, vals, reward, done): @@ -79,17 +83,17 @@ class Agent: weighted_probs = advantage[batch] * prob_ratio weighted_clipped_probs = T.clamp( prob_ratio, 1-self.policy_clip, 1+self.policy_clip)*advantage[batch] - actor_loss = -T.min(weighted_probs, - weighted_clipped_probs).mean() + self.actor_loss = -T.min(weighted_probs, + weighted_clipped_probs).mean() returns = advantage[batch] + values[batch] - critic_loss = (returns - critic_value)**2 - critic_loss = critic_loss.mean() + self.critic_loss = (returns - critic_value)**2 + self.critic_loss = self.critic_loss.mean() - total_loss = actor_loss + 0.5*critic_loss + self.total_loss = self.actor_loss + 0.5*self.critic_loss self.actor.optimizer.zero_grad() self.critic.optimizer.zero_grad() - total_loss.backward() + self.total_loss.backward() self.actor.optimizer.step() self.critic.optimizer.step() diff --git a/agents/ppo/brain.py b/agents/ppo/brain.py index 3c97910..aa0728c 100644 --- a/agents/ppo/brain.py +++ b/agents/ppo/brain.py @@ -78,11 +78,13 @@ class ActorNetwork(nn.Module): return dist - def save_checkpoint(self, filename = 'actor_torch_ppo'): + def save_checkpoint(self, filename='actor_torch_ppo'): T.save(self.state_dict(), os.path.join(self.chkpt_dir, filename)) - def load_checkpoint(self, filename = 'actor_torch_ppo'): - self.load_state_dict(T.load(os.path.join(self.chkpt_dir, filename), map_location=self.device)) + def load_checkpoint(self, filename='actor_torch_ppo'): + self.load_state_dict( + T.load(os.path.join(self.chkpt_dir, filename), + map_location=self.device)) class CriticNetwork(nn.Module): @@ -110,8 +112,10 @@ class CriticNetwork(nn.Module): value = self.critic(state) return value - def save_checkpoint(self, filename = 'critic_torch_ppo'): + def save_checkpoint(self, filename='critic_torch_ppo'): T.save(self.state_dict(), os.path.join(self.chkpt_dir, filename)) - def load_checkpoint(self, filename = 'critic_torch_ppo'): - self.load_state_dict(T.load(os.path.join(self.chkpt_dir, filename), map_location=self.device)) + def load_checkpoint(self, filename='critic_torch_ppo'): + self.load_state_dict( + T.load(os.path.join(self.chkpt_dir, filename), + map_location=self.device)) diff --git a/agents/saved_models/A0 b/agents/saved_models/A0 new file mode 100644 index 0000000..e2e5a71 Binary files /dev/null and b/agents/saved_models/A0 differ diff --git a/agents/saved_models/C0 b/agents/saved_models/C0 new file mode 100644 index 0000000..56aba3a Binary files /dev/null and b/agents/saved_models/C0 differ diff --git a/agents/saved_models/README.md b/agents/saved_models/README.md new file mode 100644 index 0000000..832735a --- /dev/null +++ b/agents/saved_models/README.md @@ -0,0 +1 @@ +# This is a folder with all the saved models. diff --git a/assets/map/Entities.csv b/assets/map/Entities.csv index 98bb2f7..5ac0ad4 100644 --- a/assets/map/Entities.csv +++ b/assets/map/Entities.csv @@ -5,40 +5,40 @@ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,392,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,391,-1,-1,-1,391,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,391,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,400,400,-1,-1,400,-1,400,-1,-1,400,-1,-1,-1,400,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,500,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,391,-1,-1,-1,391,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,391,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,500,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,390,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,393,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,400,-1,-1,400,-1,-1,-1,-1,400,-1,-1,-1,390,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,391,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,390,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,391,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,400,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,393,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,392,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 diff --git a/assets/map/FloorBlocks.csv b/assets/map/FloorBlocks.csv index 2727b5a..9bb2b04 100644 --- a/assets/map/FloorBlocks.csv +++ b/assets/map/FloorBlocks.csv @@ -3,11 +3,11 @@ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,395,395,-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,395,-1,-1,395,395,395,395,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,395,395,395,395,395,395,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,395,395,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 --1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,395,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,395,395,395,395,395,-1,-1,-1 +-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,395,-1,-1,-1 +-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,700,-1,395,-1,-1,-1 +-1,-1,-1,-1,-1,-1,395,395,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,395,-1,-1,-1 +-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,-1,-1,395,395,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,395,395,395,395,395,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,395,395,-1,-1,-1,395,395,395,395,395,395,395,395,-1,-1,395,395,395,395,395,395,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,395,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,395,-1,-1,395,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,395,-1,-1,-1,-1,-1,395,395,395,395,395,395,395,395,395,395,395,395,395,-1,-1,395,395,395,395,395,395,395,395,395,395,395,395,395,395,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 diff --git a/camera.py b/camera.py new file mode 100644 index 0000000..12ee9c5 --- /dev/null +++ b/camera.py @@ -0,0 +1,61 @@ +import os +import pygame + +from utils.resource_loader import import_assets + + +class Camera(pygame.sprite.Group): + + def __init__(self): + super().__init__() + + # General Setup + self.display_surface = pygame.display.get_surface() + self.half_width = self.display_surface.get_size()[0] // 2 + self.half_height = self.display_surface.get_size()[1] // 2 + self.offset = pygame.math.Vector2(100, 200) + + # Creating the floor + image_path = import_assets(os.path.join('graphics', + 'tilemap', + 'ground.png')) + + self.floor_surf = pygame.image.load( + import_assets( + os.path.join('graphics', + 'tilemap', + 'ground.png') + ) + ).convert() + + self.floor_rect = self.floor_surf.get_rect(topleft=(0, 0)) + + def custom_draw(self, entity): + + self.sprite_type = entity.sprite_type + # Getting the offset + if hasattr(entity, 'animation'): + self.offset.x = entity.animation.rect.centerx - self.half_width + + self.offset.y = entity.animation.rect.centery - self.half_height + + else: + self.offset.x = entity.rect.centerx - self.half_width + + self.offset.y = entity.rect.centery - self.half_height + + # Drawing the floor + floor_offset_pos = self.floor_rect.topleft - self.offset + self.display_surface.blit(self.floor_surf, floor_offset_pos) + + for sprite in sorted(self.sprites(), + key=lambda sprite: sprite.animation.rect.centery + if hasattr(sprite, 'animation') + else sprite.rect.centery): + + if hasattr(sprite, 'animation'): + offset_pos = sprite.animation.rect.topleft - self.offset + self.display_surface.blit(sprite.animation.image, offset_pos) + else: + offset_pos = sprite.rect.topleft - self.offset + self.display_surface.blit(sprite.image, offset_pos) diff --git a/configs/game/monster_config.py b/configs/game/monster_config.py index 0b77079..890097f 100644 --- a/configs/game/monster_config.py +++ b/configs/game/monster_config.py @@ -1,12 +1,41 @@ -import os - - -script_dir = os.path.dirname(os.path.abspath(__file__)) -asset_path = os.path.join( - script_dir, '../..', 'assets') - monster_data = { - 'squid': {'id': 1, 'health': .1, 'exp': 1, 'attack': .5, 'attack_type': 'slash', 'speed': 3, 'knockback': 20, 'attack_radius': 80, 'notice_radius': 360}, - 'raccoon': {'id': 2, 'health': .3, 'exp': 2.5, 'attack': .8, 'attack_type': 'claw', 'speed': 2, 'knockback': 20, 'attack_radius': 120, 'notice_radius': 400}, - 'spirit': {'id': 3, 'health': .1, 'exp': 1.1, 'attack': .6, 'attack_type': 'thunder', 'speed': 4, 'knockback': 20, 'attack_radius': 60, 'notice_radius': 350}, - 'bamboo': {'id': 4, 'health': .07, 'exp': 1.2, 'attack': .2, 'attack_type': 'leaf_attack', 'speed': 3, 'knockback': 20, 'attack_radius': 50, 'notice_radius': 300}} + 'squid': {'id': 1, + 'health': .1, + 'exp': 1, + 'attack': .5, + 'attack_type': 'slash', + 'speed': 3, + 'knockback': 20, + 'attack_radius': 80, + 'notice_radius': 360}, + + 'raccoon': {'id': 2, + 'health': .3, + 'exp': 2.5, + 'attack': .8, + 'attack_type': 'claw', + 'speed': 2, + 'knockback': 20, + 'attack_radius': 120, + 'notice_radius': 400}, + + 'spirit': {'id': 3, + 'health': .1, + 'exp': 1.1, + 'attack': .6, + 'attack_type': 'thunder', + 'speed': 4, + 'knockback': 20, + 'attack_radius': 60, + 'notice_radius': 350}, + + 'bamboo': {'id': 4, + 'health': .07, + 'exp': 1.2, + 'attack': .2, + 'attack_type': 'leaf_attack', + 'speed': 3, + 'knockback': 20, + 'attack_radius': 50, + 'notice_radius': 300} +} diff --git a/configs/game/spell_config.py b/configs/game/spell_config.py index c1febfc..3f74a9d 100644 --- a/configs/game/spell_config.py +++ b/configs/game/spell_config.py @@ -1,9 +1,22 @@ import os -script_dir = os.path.dirname(os.path.abspath(__file__)) -asset_path = os.path.join( - script_dir, '../..', 'assets') +from utils.resource_loader import import_assets + magic_data = { - 'flame': {'strength': 5, 'cost': .020, 'graphic': f"{asset_path}/graphics/particles/flame/fire.png"}, - 'heal': {'strength': 20, 'cost': .010, 'graphic': f"{asset_path}/graphics/particles/heal/heal.png"}} + 'flame': {'strength': 5, 'cost': .020, 'graphic': import_assets( + os.path.join('graphics', + 'particles', + 'flame', + 'fire.png') + ) + }, + + 'heal': {'strength': 20, 'cost': .010, 'graphic': import_assets( + os.path.join('graphics', + 'particles', + 'heal', + 'heal.png') + ) + } +} diff --git a/configs/game/weapon_config.py b/configs/game/weapon_config.py index 123dda4..3eb1b42 100644 --- a/configs/game/weapon_config.py +++ b/configs/game/weapon_config.py @@ -1,13 +1,43 @@ import os -script_dir = os.path.dirname(os.path.abspath(__file__)) -asset_path = os.path.join( - script_dir, '../..', 'assets') +from utils.resource_loader import import_assets + weapon_data = { - 'sword': {'cooldown': 100, 'damage': 15, 'graphic': f"{asset_path}/graphics/weapons/sword/full.png"}, - 'lance': {'cooldown': 400, 'damage': 30, 'graphic': f"{asset_path}/graphics/weapons/lance/full.png"}, - 'axe': {'cooldown': 300, 'damage': 20, 'graphic': f"{asset_path}/graphics/weapons/axe/full.png"}, - 'rapier': {'cooldown': 50, 'damage': 8, 'graphic': f"{asset_path}/graphics/weapons/rapier/full.png"}, - 'sai': {'cooldown': 80, 'damage': 10, 'graphic': f"{asset_path}/graphics/weapons/sai/full.png"} + 'sword': {'cooldown': 100, 'damage': 15, 'graphic': import_assets( + os.path.join('graphics', + 'weapons', + 'sword', + 'full.png') + ) + }, + + 'lance': {'cooldown': 400, 'damage': 30, 'graphic': import_assets( + os.path.join('graphics', + 'weapons', + 'lance', + 'full.png') + ) + }, + 'axe': {'cooldown': 300, 'damage': 20, 'graphic': import_assets( + os.path.join('graphics', + 'weapons', + 'axe', + 'full.png') + ) + }, + 'rapier': {'cooldown': 50, 'damage': 8, 'graphic': import_assets( + os.path.join('graphics', + 'weapons', + 'rapier', + 'full.png') + ) + }, + 'sai': {'cooldown': 80, 'damage': 10, 'graphic': import_assets( + os.path.join('graphics', + 'weapons', + 'sai', + 'full.png') + ) + }, } diff --git a/effects/magic_effects.py b/effects/magic_effects.py index a35127e..2443715 100644 --- a/effects/magic_effects.py +++ b/effects/magic_effects.py @@ -1,4 +1,3 @@ -import os import pygame from random import randint @@ -17,9 +16,14 @@ class MagicPlayer: if player.stats.health >= player.stats.stats['health']: player.stats.health = player.stats.stats['health'] self.animation_player.generate_particles( - 'aura', player.rect.center, groups) + 'aura', + player.rect.center, + groups) + self.animation_player.generate_particles( - 'heal', player.rect.center + pygame.math.Vector2(0, -50), groups) + 'heal', + player.rect.center + pygame.math.Vector2(0, -50), + groups) def flame(self, player, cost, groups): if player.stats.energy >= cost: diff --git a/effects/particle_effects.py b/effects/particle_effects.py index 2203134..2faed8f 100644 --- a/effects/particle_effects.py +++ b/effects/particle_effects.py @@ -8,49 +8,118 @@ from random import choice class AnimationPlayer: def __init__(self): - script_dir = os.path.dirname(os.path.abspath(__file__)) - asset_path = os.path.join( - script_dir, '..', 'assets') - self.frames = { - # magic - 'flame': import_folder(f'{asset_path}/graphics/particles/flame/frames'), - 'aura': import_folder(f'{asset_path}/graphics/particles/aura'), - 'heal': import_folder(f'{asset_path}/graphics/particles/heal/frames'), + # Spells + 'flame': import_folder(os.path.join('graphics', + 'particles', + 'flame', + 'frames')), - # attacks - 'claw': import_folder(f'{asset_path}/graphics/particles/claw'), - 'slash': import_folder(f'{asset_path}/graphics/particles/slash'), - 'sparkle': import_folder(f'{asset_path}/graphics/particles/sparkle'), - 'leaf_attack': import_folder(f'{asset_path}/graphics/particles/leaf_attack'), - 'thunder': import_folder(f'{asset_path}/graphics/particles/thunder'), + 'aura': import_folder(os.path.join('graphics', + 'particles', + 'aura')), - # monster deaths - 'squid': import_folder(f'{asset_path}/graphics/particles/smoke_orange'), - 'raccoon': import_folder(f'{asset_path}/graphics/particles/raccoon'), - 'spirit': import_folder(f'{asset_path}/graphics/particles/nova'), - 'bamboo': import_folder(f'{asset_path}/graphics/particles/bamboo'), + 'heal': import_folder(os.path.join('graphics', + 'particles', + 'heal', + 'frames')), - # leafs + # Attacks + 'claw': import_folder(os.path.join('graphics', + 'particles', + 'claw')), + + 'slash': import_folder(os.path.join('graphics', + 'particles', + 'slash')), + + 'sparkle': import_folder(os.path.join('graphics', + 'particles', + 'sparkle')), + + 'leaf_attack': import_folder(os.path.join('graphics', + 'particles', + 'leaf_attack')), + 'thunder': import_folder(os.path.join('graphics', + 'particles', + 'thunder')), + + # Monster Deaths + 'squid': import_folder(os.path.join('graphics', + 'particles', + 'smoke_orange')), + + 'raccoon': import_folder(os.path.join('graphics', + 'particles', + 'raccoon')), + + 'spirit': import_folder(os.path.join('graphics', + 'particles', + 'nova')), + + 'bamboo': import_folder(os.path.join('graphics', + 'particles', + 'bamboo')), + + # Leafs 'leaf': ( - import_folder(f'{asset_path}/graphics/particles/leaf1'), - import_folder(f'{asset_path}/graphics/particles/leaf2'), - import_folder(f'{asset_path}/graphics/particles/leaf3'), - import_folder(f'{asset_path}/graphics/particles/leaf4'), - import_folder(f'{asset_path}/graphics/particles/leaf5'), - import_folder(f'{asset_path}/graphics/particles/leaf6'), - self.reflect_images(import_folder( - f'{asset_path}/graphics/particles/leaf1')), - self.reflect_images(import_folder( - f'{asset_path}/graphics/particles/leaf2')), - self.reflect_images(import_folder( - f'{asset_path}/graphics/particles/leaf3')), - self.reflect_images(import_folder( - f'{asset_path}/graphics/particles/leaf4')), - self.reflect_images(import_folder( - f'{asset_path}/graphics/particles/leaf5')), - self.reflect_images(import_folder( - f'{asset_path}/graphics/particles/leaf6')) + import_folder(os.path.join('graphics', + 'particles', + 'leaf1')), + + import_folder(os.path.join('graphics', + 'particles', + 'leaf2')), + + import_folder(os.path.join('graphics', + 'particles', + 'leaf3')), + + import_folder(os.path.join('graphics', + 'particles', + 'leaf4')), + + import_folder(os.path.join('graphics', + 'particles', + 'leaf5')), + + import_folder(os.path.join('graphics', + 'particles', + 'leaf6')), + + self.reflect_images( + import_folder(os.path.join('graphics', + 'particles', + 'leaf1'))), + + self.reflect_images( + import_folder(os.path.join('graphics', + 'particles', + 'leaf2'))), + + self.reflect_images( + import_folder( + os.path.join('graphics', + 'particles', + 'leaf3'))), + + self.reflect_images( + import_folder( + os.path.join('graphics', + 'particles', + 'leaf4'))), + + self.reflect_images( + import_folder( + os.path.join('graphics', + 'particles', + 'leaf5'))), + + self.reflect_images( + import_folder( + os.path.join('graphics', + 'particles', + 'leaf6'))) ) } diff --git a/effects/weapon_effects.py b/effects/weapon_effects.py index 6a1997d..2eada75 100644 --- a/effects/weapon_effects.py +++ b/effects/weapon_effects.py @@ -1,22 +1,24 @@ import os import pygame +from utils.resource_loader import import_assets + class Weapon(pygame.sprite.Sprite): def __init__(self, player, groups): super().__init__(groups) - script_dir = os.path.dirname(os.path.abspath(__file__)) - asset_path = os.path.join( - script_dir, '..', 'assets') - self.sprite_type = 'weapon' direction = player._input.status.split('_')[0] # Graphic - full_path = f"{asset_path}/graphics/weapons/{player._input.combat.weapon}/{direction}.png" - self.image = pygame.image.load(full_path).convert_alpha() + self.image = pygame.image.load(import_assets(os.path.join( + 'graphics', + 'weapons', + player._input.combat.weapon, + f"{direction}.png")) + ).convert_alpha() # Sprite Placement if direction == 'right': diff --git a/entities/__init__.py b/entities/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/entities/components/_input.py b/entities/components/_input.py index af8bd35..f12d4ee 100644 --- a/entities/components/_input.py +++ b/entities/components/_input.py @@ -38,9 +38,13 @@ class InputHandler: self.possible_actions = [0, 1, 2, 3, 4, 5] self.action = 10 - def check_input(self, button, speed, hitbox, obstacle_sprites, rect, player): - - self.action = 10 + def check_input(self, + button, + speed, + hitbox, + obstacle_sprites, + rect, + player): if not self.attacking and self.can_move: @@ -105,9 +109,13 @@ class InputHandler: # Rotating Weapons if button == 6 and self.can_rotate_weapon: + self.can_rotate_weapon = False self.weapon_rotation_time = pygame.time.get_ticks() - if self.combat.weapon_index < len(list(weapon_data.keys())) - 1: + + if self.combat.weapon_index\ + < len(list(weapon_data.keys())) - 1: + self.combat.weapon_index += 1 else: self.combat.weapon_index = 0 @@ -131,23 +139,34 @@ class InputHandler: self.vulnerable = vulnerable if self.attacking: - if current_time - self.attack_time > self.attack_cooldown + weapon_data[self.combat.weapon]['cooldown']: + if current_time - self.attack_time\ + > self.attack_cooldown\ + + weapon_data[self.combat.weapon]['cooldown']: + self.attacking = False if self.combat.current_attack: self.combat.delete_attack_sprite() if not self.can_rotate_weapon: - if current_time - self.weapon_rotation_time > self.rotate_attack_cooldown: + if current_time - self.weapon_rotation_time\ + > self.rotate_attack_cooldown: + self.can_rotate_weapon = True if not self.can_swap_magic: - if current_time - self.magic_swap_time > self.rotate_attack_cooldown: + if current_time - self.magic_swap_time\ + > self.rotate_attack_cooldown: + self.can_swap_magic = True if not vulnerable: - if current_time - self.combat.hurt_time >= self.combat.invulnerability_duration: + if current_time - self.combat.hurt_time\ + >= self.combat.invulnerability_duration: + self.combat.vulnerable = True if not self.can_move: - if current_time - self.move_time >= self.move_cooldown: + if current_time - self.move_time\ + >= self.move_cooldown: + self.can_move = True diff --git a/entities/components/animaton.py b/entities/components/animaton.py index f1148c4..30f616d 100644 --- a/entities/components/animaton.py +++ b/entities/components/animaton.py @@ -2,7 +2,7 @@ import os import pygame from math import sin -from utils.resource_loader import import_folder +from utils.resource_loader import import_folder, import_assets from configs.system.window_config import HITBOX_OFFSET @@ -17,40 +17,47 @@ class AnimationHandler: self.name = name def import_assets(self, position): - script_dir = os.path.dirname(os.path.abspath(__file__)) - asset_path = os.path.join( - script_dir, '../..', 'assets', 'graphics') + + # Import Graphic Assets if self.sprite_type == 'player': - - character_path = f"{asset_path}/player" - - # Import Graphic Assets self.image = pygame.image.load( - f"{character_path}/down/down_0.png").convert_alpha() + import_assets(os.path.join('graphics', + 'player', + 'down', + 'down_0.png'))).convert_alpha() + self.rect = self.image.get_rect(topleft=position) self.hitbox = self.rect.inflate(HITBOX_OFFSET[self.sprite_type]) self.animations = { - 'up': [], 'down': [], 'left': [], 'right': [], - 'up_idle': [], 'down_idle': [], 'left_idle': [], 'right_idle': [], - 'up_attack': [], 'down_attack': [], 'left_attack': [], 'right_attack': [] + 'up': [], 'down': [], + 'left': [], 'right': [], + 'up_idle': [], 'down_idle': [], + 'left_idle': [], 'right_idle': [], + 'up_attack': [], 'down_attack': [], + 'left_attack': [], 'right_attack': [] } + for animation in self.animations.keys(): - full_path = f"{character_path}/{animation}" - self.animations[animation] = import_folder(full_path) + self.animations[animation]\ + = import_folder(os.path.join('graphics', + 'player', + animation + )) elif self.sprite_type == 'enemy': self.status = 'idle' - character_path = f"{asset_path}/monsters/{self.name}" - self.animations = {'idle': [], 'move': [], 'attack': []} for animation in self.animations.keys(): - self.animations[animation] = import_folder( - f"{character_path}/{animation}") + self.animations[animation]\ + = import_folder(os.path.join('graphics', + 'monsters', + self.name, + animation)) self.image = self.animations[self.status][self.frame_index] self.rect = self.image.get_rect(topleft=position) diff --git a/entities/enemy.py b/entities/enemy.py index 40be325..b9861d3 100644 --- a/entities/enemy.py +++ b/entities/enemy.py @@ -11,11 +11,12 @@ class Enemy(pygame.sprite.Sprite): def __init__(self, name, position, groups, visible_sprites, obstacle_sprites): super().__init__(groups) + self.sprite_type = "enemy" self.name = name + self.visible_sprites = visible_sprites - self.position = position # Setup Graphics self.animation_player = AnimationPlayer() self.animation = AnimationHandler(self.sprite_type, self.name) diff --git a/entities/observer.py b/entities/observer.py index 1578bb9..ae53fae 100644 --- a/entities/observer.py +++ b/entities/observer.py @@ -3,6 +3,8 @@ import pygame from configs.system.window_config import HITBOX_OFFSET +from utils.resource_loader import import_assets + class Observer(pygame.sprite.Sprite): @@ -11,12 +13,11 @@ class Observer(pygame.sprite.Sprite): self.sprite_type = 'camera' - script_dir = os.path.dirname(os.path.abspath(__file__)) - asset_path = os.path.join( - script_dir, '..', 'assets') - self.image = pygame.image.load( - f"{asset_path}/graphics/observer.png").convert_alpha() + import_assets(os.path.join('graphics', + 'observer.png')) + ).convert_alpha() + self.rect = self.image.get_rect(topleft=position) self.hitbox = self.rect.inflate(HITBOX_OFFSET[self.sprite_type]) diff --git a/entities/player.py b/entities/player.py index 3fc0360..801aec1 100644 --- a/entities/player.py +++ b/entities/player.py @@ -1,3 +1,4 @@ +import os import pygame import numpy as np from random import randint @@ -15,66 +16,101 @@ from agents.ppo.agent import Agent class Player(pygame.sprite.Sprite): - def __init__(self, + player_id, + role, position, groups, obstacle_sprites, visible_sprites, attack_sprites, - attackable_sprites, - role, - player_id): - + attackable_sprites + ): super().__init__(groups) - # Setup Sprites - self.sprite_type = 'player' - self.status = 'down' + self.initial_position = position self.player_id = player_id + self.distance_direction_from_enemy = None + + # Sprite Setup + self.sprite_type = "player" + self.obstacle_sprites = obstacle_sprites self.visible_sprites = visible_sprites self.attack_sprites = attack_sprites - self.obstacle_sprites = obstacle_sprites self.attackable_sprites = attackable_sprites - # Setup Graphics + # Graphics Setup self.animation_player = AnimationPlayer() self.animation = AnimationHandler(self.sprite_type) self.animation.import_assets(position) - self.image = self.animation.image - self.rect = self.animation.rect - # Setup Inputs + # Input Setup self._input = InputHandler( - self.sprite_type, self.animation_player) # , self.status) + self.sprite_type, self.animation_player) # Setup Stats self.role = role self.stats = StatsHandler(self.sprite_type, self.role) - self.distance_direction_from_enemy = None + def setup_agent(self, + gamma, + alpha, + policy_clip, + batch_size, + N, + n_epochs, + gae_lambda, + chkpt_dir, + no_load=False): - # Setup AI - self.score = 0 - self.learn_iters = 0 - self.n_steps = 0 - self.N = 20 + self.get_current_state() + self.agent = Agent( + input_dims=len(self.state_features), + n_actions=len(self._input.possible_actions), + gamma=gamma, + alpha=alpha, + policy_clip=policy_clip, + batch_size=batch_size, + N=N, + n_epochs=n_epochs, + gae_lambda=gae_lambda, + chkpt_dir=chkpt_dir + ) + print( + f"\nAgent initialized on player {self.player_id} using {self.agent.actor.device}.") + + if not no_load: + print("Attempting to load models ...") + try: + self.agent.load_models( + actr_chkpt=f"A{self.player_id}", + crtc_chkpt=f"C{self.player_id}" + ) + print("Models loaded ...\n") + + except FileNotFoundError: + print( + f"FileNotFound for player {self.player_id}.\ + \nSkipping loading ...\n") def get_status(self): - if self._input.movement.direction.x == 0 and self._input.movement.direction.y == 0: - if 'idle' not in self.status and 'attack' not in self.status: - self.status += '_idle' + if self._input.movement.direction.x == 0\ + and self._input.movement.direction.y == 0: + + if 'idle' not in self._input.status and 'attack' not in self._input.status: + self._input.status += '_idle' if self._input.attacking: self._input.movement.direction.x = 0 self._input.movement.direction.y = 0 - if 'attack' not in self.status: - if 'idle' in self.status: - self.status = self.status.replace('idle', 'attack') + if 'attack' not in self._input.status: + if 'idle' in self._input.status: + self._input.status = self._input.status.replace( + 'idle', 'attack') else: - self.status += '_attack' + self._input.status += '_attack' else: - if 'attack' in self.status: - self.status = self.status.replace('_attack', '') + if 'attack' in self._input.status: + self._input.status = self._input.status.replace('_attack', '') def attack_logic(self): if self.attack_sprites: @@ -123,11 +159,12 @@ class Player(pygame.sprite.Sprite): 2*np.exp(-nearest_dist**2), np.exp(-nearest_enemy.stats.health), -np.exp(-self.stats.health**2) + if not self.is_dead() > 0 else -1 ] self.state_features = [ - np.exp(-self.rect.center[0]), - np.exp(-self.rect.center[1]), + np.exp(-self.animation.rect.center[0]), + np.exp(-self.animation.rect.center[1]), self._input.movement.direction.x, self._input.movement.direction.y, self.stats.health/self.stats.stats['health'], @@ -153,73 +190,48 @@ class Player(pygame.sprite.Sprite): self.state_features = np.array(self.state_features) - def get_max_num_states(self): - self.get_current_state() - self.num_features = len(self.state_features) - - def setup_agent(self): - print(f"Initializing agent on player {self.player_id} ...") - self.agent = Agent( - input_dims=len(self.state_features), - n_actions=len(self._input.possible_actions), - batch_size=5, - n_epochs=4) - print( - f" Agent initialized using {self.agent.actor.device}. Attempting to load models ...") - - try: - self.agent.load_models( - actr_chkpt=f"player_actor", crtc_chkpt=f"player_critic") - print("Models loaded ...\n") - - except FileNotFoundError: - print("FileNotFound for agent. Skipping loading...\n") - def is_dead(self): if self.stats.health <= 0: + self.stats.health = 0 + self.animation.import_assets((3264, 448)) return True else: return False def update(self): - # Get the current state - self.get_current_state() - # Choose action based on current state - action, probs, value = self.agent.choose_action(self.state_features) + if not self.is_dead(): + # Get the current state + self.get_current_state() + # Choose action based on current state + action, probs, value\ + = self.agent.choose_action(self.state_features) - self.n_steps += 1 - # Apply chosen action - self._input.check_input(action, - self.stats.speed, - self.animation.hitbox, - self.obstacle_sprites, - self.animation.rect, - self) + # Apply chosen action + self._input.check_input(action, + self.stats.speed, + self.animation.hitbox, + self.obstacle_sprites, + self.animation.rect, + self) - self.score = self.stats.exp - self.agent.remember(self.state_features, action, - probs, value, self.stats.exp, self.is_dead()) + self.score = self.stats.exp + self.agent.remember(self.state_features, action, + probs, value, self.stats.exp, self.is_dead()) - if self.n_steps % self.N == 0: - self.agent.learn() - self.learn_iters += 1 + self.get_current_state() - self.get_current_state() + # Cooldowns and Regen + self.stats.health_recovery() + self.stats.energy_recovery() - # Refresh objects based on input - self.status = self._input.status + else: + self.stats.exp = max(0, self.stats.exp - .01) - # Animate + # Refresh player based on input and animate self.get_status() - self.animation.animate(self.status, self._input.combat.vulnerable) + self.animation.animate( + self._input.status, self._input.combat.vulnerable) self.image = self.animation.image self.rect = self.animation.rect - - # Cooldowns and Regen - self.stats.health_recovery() - self.stats.energy_recovery() self._input.cooldowns(self._input.combat.vulnerable) - - if self.is_dead(): - self.stats.exp = max(-1, self.stats.exp - .5) diff --git a/level/terrain.py b/entities/terrain.py similarity index 56% rename from level/terrain.py rename to entities/terrain.py index 9de2c1f..ac02b77 100644 --- a/level/terrain.py +++ b/entities/terrain.py @@ -1,15 +1,26 @@ import pygame -from configs.system.window_config import * +from configs.system.window_config import TILESIZE,\ + HITBOX_OFFSET -class Tile(pygame.sprite.Sprite): +class Terrain(pygame.sprite.Sprite): + + def __init__(self, + position, + groups, + sprite_type, + surface=pygame.Surface((TILESIZE, TILESIZE)) + ): - def __init__(self, position, groups, sprite_type, surface=pygame.Surface((TILESIZE, TILESIZE))): super().__init__(groups) self.sprite_type = sprite_type + + self.position = position + self.image = surface + if sprite_type == 'object': # Offset self.rect = self.image.get_rect( diff --git a/figures/actor_loss.png b/figures/actor_loss.png new file mode 100644 index 0000000..78ba7b8 Binary files /dev/null and b/figures/actor_loss.png differ diff --git a/figures/critic_loss.png b/figures/critic_loss.png new file mode 100644 index 0000000..6cc5bc9 Binary files /dev/null and b/figures/critic_loss.png differ diff --git a/figures/score.png b/figures/score.png new file mode 100644 index 0000000..27ce63c Binary files /dev/null and b/figures/score.png differ diff --git a/figures/total_loss.png b/figures/total_loss.png new file mode 100644 index 0000000..f48efb8 Binary files /dev/null and b/figures/total_loss.png differ diff --git a/game.py b/game.py index 4a3a707..3905e6b 100644 --- a/game.py +++ b/game.py @@ -1,49 +1,58 @@ -import pygame +import os import sys +import pygame -from level.level import Level -from configs.system.window_config import WIDTH, HEIGHT, WATER_COLOR, FPS +from level import Level + +from configs.system.window_config import WIDTH,\ + HEIGHT,\ + WATER_COLOR,\ + FPS class Game: - def __init__(self, n_players): + def __init__(self, show_pg=False, n_players=1,): + print(f"Initializing Pneuma with {n_players} player(s).\ + \nShowing PyGame screen: {'True' if show_pg else 'False'}") pygame.init() - self.screen = pygame.display.set_mode( - (WIDTH, HEIGHT)) # , pygame.HIDDEN) + if show_pg: - pygame.display.set_caption('Pneuma') + self.screen = pygame.display.set_mode( + (WIDTH, HEIGHT) + ) - img = pygame.image.load('assets/graphics/icon.png') + else: + self.screen = pygame.display.set_mode( + (WIDTH, HEIGHT), + pygame.HIDDEN + ) + + pygame.display.set_caption("Pneuma") + + img = pygame.image.load(os.path.join('assets', + 'graphics', + 'icon.png')) pygame.display.set_icon(img) - self.clock = pygame.time.Clock() self.level = Level(n_players) - self.max_num_players = len(self.level.player_sprites) - - def calc_score(self): - - self.scores = [0 for _ in range(self.max_num_players)] - - for player in self.level.player_sprites: - self.scores[player.player_id] = player.stats.exp - def run(self): + self.clock = pygame.time.Clock() + for event in pygame.event.get(): if event.type == pygame.QUIT: self.quit() - if event.type == pygame.KEYDOWN: + elif event.type == pygame.KEYDOWN: if event.key == pygame.K_m: - self.level.toggle_menu() - self.level.observer.update() + self.level.pause() self.screen.fill(WATER_COLOR) - self.level.run() + self.level.run('observer', self.clock.get_fps()) pygame.display.update() self.clock.tick(FPS) diff --git a/interface/ui.py b/interface/ui.py index d504c5b..a01d7c2 100644 --- a/interface/ui.py +++ b/interface/ui.py @@ -1,18 +1,25 @@ import pygame -from configs.game.weapon_config import * -from configs.game.spell_config import * +from configs.game.weapon_config import weapon_data +from configs.game.spell_config import magic_data -from .ui_settings import * +from .ui_settings import UI_FONT,\ + UI_FONT_SIZE,\ + HEALTH_BAR_WIDTH,\ + HEALTH_COLOR,\ + ENERGY_BAR_WIDTH,\ + ENERGY_COLOR,\ + BAR_HEIGHT,\ + UI_BG_COLOR,\ + UI_BORDER_COLOR_ACTIVE,\ + UI_BORDER_COLOR,\ + TEXT_COLOR,\ + ITEM_BOX_SIZE class UI: def __init__(self): - script_dir = os.path.dirname(os.path.abspath(__file__)) - asset_path = os.path.join( - script_dir, '../..', 'assets') - # General info self.display_surface = pygame.display.get_surface() self.font = pygame.font.Font(UI_FONT, UI_FONT_SIZE) @@ -66,7 +73,7 @@ class UI: pygame.draw.rect(self.display_surface, UI_BORDER_COLOR, text_rect.inflate(10, 10), 4) else: - text_surf = self.font.render(f"OBSERVER", False, TEXT_COLOR) + text_surf = self.font.render("OBSERVER", False, TEXT_COLOR) x = self.display_surface.get_size()[0] - 20 y = self.display_surface.get_size()[1] - 20 text_rect = text_surf.get_rect(bottomright=(x, y)) @@ -104,9 +111,17 @@ class UI: def display(self, player): if player.sprite_type == 'player': self.show_bar( - player.stats.health, player.stats.stats['health'], self.health_bar_rect, HEALTH_COLOR) + player.stats.health, + player.stats.stats['health'], + self.health_bar_rect, + HEALTH_COLOR) + self.show_bar( - player.stats.energy, player.stats.stats['energy'], self.energy_bar_rect, ENERGY_COLOR) + player.stats.energy, + player.stats.stats['energy'], + self.energy_bar_rect, + ENERGY_COLOR) + self.show_exp(player.stats.exp) self.weapon_overlay(player._input.combat.weapon_index, player._input.can_rotate_weapon) diff --git a/interface/ui_settings.py b/interface/ui_settings.py index eb6bfa6..c45fe59 100644 --- a/interface/ui_settings.py +++ b/interface/ui_settings.py @@ -1,29 +1,26 @@ import os +from utils.resource_loader import import_assets -script_dir = os.path.dirname(os.path.abspath(__file__)) -asset_path = os.path.join( - script_dir, '..', 'assets') - -# ui +# UI BAR_HEIGHT = 20 HEALTH_BAR_WIDTH = 200 ENERGY_BAR_WIDTH = 140 ITEM_BOX_SIZE = 80 -UI_FONT = f"{asset_path}/font/joystix.ttf" +UI_FONT = import_assets(path=os.path.join('font', 'joystix.ttf')) UI_FONT_SIZE = 18 -# general colors +# General Colors WATER_COLOR = '#71ddee' UI_BG_COLOR = '#222222' UI_BORDER_COLOR = '#111111' TEXT_COLOR = '#EEEEEE' -# ui colors +# UI Colors HEALTH_COLOR = 'red' ENERGY_COLOR = 'blue' UI_BORDER_COLOR_ACTIVE = 'gold' -# Upgrade menu +# Upgrade Menu TEXT_COLOR_SELECTED = '#111111' BAR_COLOR = '#EEEEEE' BAR_COLOR_SELECTED = '#111111' diff --git a/interface/upgrade.py b/interface/upgrade.py index d27a857..17cf502 100644 --- a/interface/upgrade.py +++ b/interface/upgrade.py @@ -1,6 +1,14 @@ import pygame -from .ui_settings import UI_FONT, UI_FONT_SIZE, TEXT_COLOR, TEXT_COLOR_SELECTED, UPGRADE_BG_COLOR_SELECTED, UI_BORDER_COLOR, UI_BG_COLOR, BAR_COLOR_SELECTED, BAR_COLOR +from .ui_settings import UI_FONT,\ + UI_FONT_SIZE,\ + TEXT_COLOR,\ + TEXT_COLOR_SELECTED,\ + UPGRADE_BG_COLOR_SELECTED,\ + UI_BORDER_COLOR,\ + UI_BG_COLOR,\ + BAR_COLOR_SELECTED,\ + BAR_COLOR class Upgrade: diff --git a/level.py b/level.py new file mode 100644 index 0000000..ba30cef --- /dev/null +++ b/level.py @@ -0,0 +1,305 @@ +import os +import pygame +import numpy as np + +from random import choice + +from configs.system.window_config import TILESIZE + +from utils.debug import debug +from utils.resource_loader import import_csv_layout, import_folder + +from interface.ui import UI + +from entities.observer import Observer +from entities.player import Player +from entities.enemy import Enemy +from entities.terrain import Terrain + +from camera import Camera + + +class Level: + + def __init__(self, n_players): + + self.paused = False + self.done = False + + # Get display surface + self.display_surface = pygame.display.get_surface() + + # Setup Sprite groups + self.visible_sprites = Camera() + self.obstacle_sprites = pygame.sprite.Group() + self.attack_sprites = pygame.sprite.Group() + self.attackable_sprites = pygame.sprite.Group() + + # Map generation + self.n_players = n_players + self.generate_map() + + # Handle generated entities + self.get_entities() + self.get_distance_direction() + self.dead_players = np.zeros(self.n_players) + + # Setup UI + self.ui = UI() + + def generate_map(self): + + self.possible_player_locations = [] + + player_id = 0 + + self.layouts = { + 'boundary': import_csv_layout(os.path.join('map', + 'FloorBlocks.csv')), + 'grass': import_csv_layout(os.path.join('map', + 'Grass.csv')), + 'objects': import_csv_layout(os.path.join('map', + 'Objects.csv')), + 'entities': import_csv_layout(os.path.join('map', + 'Entities.csv')) + } + + self.graphics = { + 'grass': import_folder(os.path.join('graphics', 'grass')), + 'objects': import_folder(os.path.join('graphics', 'objects')) + } + + for style, layout in self.layouts.items(): + for row_index, row in enumerate(layout): + for col_index, col in enumerate(row): + if int(col) != -1: + + x = col_index * TILESIZE + y = row_index * TILESIZE + + # Generate unpassable terrain + if style == 'boundary': + if col != '700': + Terrain((x, y), + [self.obstacle_sprites, + self.visible_sprites], + 'invisible') + if col == '700': + print(f"Prison set at:{(x, y)}") + # Generate grass + if style == 'grass': + random_grass_image = choice(self.graphics['grass']) + + Terrain((x, y), [ + self.visible_sprites, + self.obstacle_sprites, + self.attackable_sprites + ], + 'grass', + random_grass_image) + + # Generate objects like trees and statues + # if style == 'objects': + # surface = self.graphics['objects'][int(col)] + # Terrain((x, y), [ + # self.visible_sprites, + # self.obstacle_sprites + # ], + # 'object', + # surface) + + # Generate observer, players and monsters + if style == 'entities': + + # Generate observer + if col == '500': + self.observer = Observer( + (x, y), + [self.visible_sprites] + ) + + # Generate player(s) + # TODO: Make a way to generate players in random locations + elif col == '400': + self.possible_player_locations.append((x, y)) + # Monster generation + + else: + if col == '390': + monster_name = 'bamboo' + elif col == '391': + monster_name = 'spirit' + elif col == '392': + monster_name = 'raccoon' + elif col == ' 393': + monster_name = 'squid' + Enemy(name=monster_name, + position=(x, y), + groups=[self.visible_sprites, + self.attackable_sprites], + visible_sprites=self.visible_sprites, + obstacle_sprites=self.obstacle_sprites) + + for player_id in range(self.n_players): + Player( + player_id, + 'tank', + choice(self.possible_player_locations), + [self.visible_sprites], + self.obstacle_sprites, + self.visible_sprites, + self.attack_sprites, + self.attackable_sprites + ) + + def reset(self): + + for grass in self.grass_sprites: + grass.kill() + + for enemy in self.enemy_sprites: + enemy.kill() + + for style, layout in self.layouts.items(): + for row_index, row in enumerate(layout): + for col_index, col in enumerate(row): + if int(col) != -1: + x = col_index * TILESIZE + y = row_index * TILESIZE + # Regenerate grass + if style == 'grass': + random_grass_image = choice( + self.graphics['grass']) + + Terrain((x, y), [ + self.visible_sprites, + self.obstacle_sprites, + self.attackable_sprites + ], + 'grass', + random_grass_image) + + if style == 'entities': + + if col == '500': + continue + + if col == '400': + continue + + else: + if col == '390': + monster_name = 'bamboo' + elif col == '391': + monster_name = 'spirit' + elif col == '392': + monster_name = 'raccoon' + elif col == ' 393': + monster_name = 'squid' + + Enemy(monster_name, + (x, y), + [self.visible_sprites, + self.attackable_sprites], + self.visible_sprites, + self.obstacle_sprites) + + for player in self.player_sprites: + player.animation.import_assets( + choice(self.possible_player_locations)) + player.stats.health\ + = player.stats.stats['health'] + player.stats.energy\ + = player.stats.stats['energy'] + + self.get_entities() + self.get_distance_direction() + self.dead_players = np.zeros(self.n_players) + self.done = False + + def get_entities(self): + + self.player_sprites = [sprite + for sprite in self.visible_sprites.sprites() + if sprite.sprite_type == 'player'] + + self.enemy_sprites = [sprite + for sprite in self.visible_sprites.sprites() + if sprite.sprite_type == 'enemy'] + + self.grass_sprites = [sprite + for sprite in self.visible_sprites.sprites() + if sprite.sprite_type == 'grass'] + + def get_distance_direction(self): + for player in self.player_sprites: + player.distance_direction_from_enemy = [] + + for enemy in self.enemy_sprites: + enemy.distance_direction_from_player = [] + + for player in self.player_sprites: + if not player.is_dead(): + player_vector = pygame.math.Vector2( + player.animation.rect.center + ) + + for enemy in self.enemy_sprites: + enemy_vector = pygame.math.Vector2( + enemy.animation.rect.center + ) + distance\ + = (player_vector - enemy_vector).magnitude() + + if distance > 0: + direction\ + = (player_vector - enemy_vector).normalize() + else: + direction\ + = pygame.math.Vector2() + + enemy.distance_direction_from_player.append( + (distance, direction, player)) + player.distance_direction_from_enemy.append( + (distance, -direction, enemy)) + + def apply_damage_to_player(self): + for enemy in self.enemy_sprites: + for distance, _, player in enemy.distance_direction_from_player: + + if (distance < enemy.stats.attack_radius + and player._input.combat.vulnerable): + + player.stats.health -= enemy.stats.attack + player._input.combat.vulnerable = False + player._input.combat.hurt_time = pygame.time.get_ticks() + + def toggle_pause(self): + self.paused = not self.paused + + def run(self, who='observer', fps='v0.9'): + # Draw the game + self.visible_sprites.custom_draw(self.observer) + self.ui.display(self.observer) + + debug(f"{fps}") + + if not self.paused: + # Update the game + for player in self.player_sprites: + if player.stats.health > 0: + player.attack_logic() + + self.get_entities() + self.get_distance_direction() + self.visible_sprites.update() + self.apply_damage_to_player() + + else: + debug('PAUSED') + + for player in self.player_sprites: + self.dead_players[player.player_id] = player.is_dead() + + self.done = True if (self.dead_players.all() == 1 + or self.enemy_sprites == []) else False diff --git a/level/__init__.py b/level/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/level/camera.py b/level/camera.py deleted file mode 100644 index 49f52b9..0000000 --- a/level/camera.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import pygame - - -class Camera(pygame.sprite.Group): - - def __init__(self): - super().__init__() - - # General Setup - self.display_surface = pygame.display.get_surface() - self.half_width = self.display_surface.get_size()[0] // 2 - self.half_height = self.display_surface.get_size()[1] // 2 - self.offset = pygame.math.Vector2(100, 200) - - # Creating the floor - script_dir = os.path.dirname(os.path.abspath(__file__)) - image_path = os.path.join( - script_dir, '..', 'assets', 'graphics', 'tilemap', 'ground.png') - - self.floor_surf = pygame.image.load(image_path).convert() - self.floor_rect = self.floor_surf.get_rect(topleft=(0, 0)) - - def custom_draw(self, player): - self.sprite_type = player.sprite_type - # Getting the offset - self.offset.x = player.rect.centerx - self.half_width - self.offset.y = player.rect.centery - self.half_height - - # Drawing the floor - floor_offset_pos = self.floor_rect.topleft - self.offset - self.display_surface.blit(self.floor_surf, floor_offset_pos) - - for sprite in sorted(self.sprites(), key=lambda sprite: sprite.rect.centery): - offset_pos = sprite.rect.topleft - self.offset - self.display_surface.blit(sprite.image, offset_pos) - - def enemy_update(self, player): - enemy_sprites = [sprite for sprite in self.sprites() if hasattr( - sprite, 'sprite_type') and sprite.sprite_type == 'enemy'] - for enemy in enemy_sprites: - enemy.enemy_update(player) diff --git a/level/level.py b/level/level.py deleted file mode 100644 index 52e440e..0000000 --- a/level/level.py +++ /dev/null @@ -1,229 +0,0 @@ -import os -import pygame -import numpy as np - -from random import choice - -from configs.system.window_config import TILESIZE - -from utils.debug import debug -from utils.resource_loader import import_csv_layout, import_folder - -from interface.ui import UI - -from entities.observer import Observer -from entities.player import Player -from entities.enemy import Enemy - -from .terrain import Tile -from .camera import Camera - - -class Level: - - def __init__(self, n_players, reset=False): - - # General Settings - self.game_paused = False - self.done = False - - # Get display surface - self.display_surface = pygame.display.get_surface() - - # Sprite Group setup - self.visible_sprites = Camera() - self.obstacle_sprites = pygame.sprite.Group() - self.attack_sprites = pygame.sprite.Group() - self.attackable_sprites = pygame.sprite.Group() - - # Sprite setup and entity generation - self.create_map(n_players) - self.get_players_enemies() - self.get_distance_direction() - if not reset: - for player in self.player_sprites: - player.get_max_num_states() - player.setup_agent() - else: - for player in self.player_sprites: - player.get_max_num_states() - self.dead_players = np.zeros(len(self.player_sprites)) - - # UI setup - self.ui = UI() - - def create_map(self, n_players): - player_id = 0 - script_dir = os.path.dirname(os.path.abspath(__file__)) - asset_path = os.path.join( - script_dir, '..', 'assets') - layouts = { - 'boundary': import_csv_layout(f"{asset_path}/map/FloorBlocks.csv"), - 'grass': import_csv_layout(f"{asset_path}/map/Grass.csv"), - 'objects': import_csv_layout(f"{asset_path}/map/Objects.csv"), - 'entities': import_csv_layout(f"{asset_path}/map/Entities.csv") - } - - graphics = { - 'grass': import_folder(f"{asset_path}/graphics/grass"), - 'objects': import_folder(f"{asset_path}/graphics/objects") - } - - for style, layout in layouts.items(): - for row_index, row in enumerate(layout): - for col_index, col in enumerate(row): - if col != '-1': - x = col_index * TILESIZE - y = row_index * TILESIZE - if style == 'boundary': - Tile((x, y), [self.obstacle_sprites], 'invisible') - - if style == 'grass': - random_grass_image = choice(graphics['grass']) - Tile((x, y), [self.visible_sprites, self.obstacle_sprites, - self.attackable_sprites], 'grass', random_grass_image) - # - # if style == 'objects': - # surf = graphics['objects'][int(col)] - # Tile((x, y), [self.visible_sprites, - # self.obstacle_sprites], 'object', surf) - - if style == 'entities': - # The numbers represent their IDs in .csv files generated from TILED. - if col == '500': - self.observer = Observer( - (x, y), [self.visible_sprites]) - - elif col == '400': - if choice([0, 1]) == 1 and player_id <= n_players: - # Player Generation - Player( - (x, y), - [self.visible_sprites], - self.obstacle_sprites, - self.visible_sprites, - self.attack_sprites, - self.attackable_sprites, - 'tank', - player_id) - - player_id += 1 - - elif col == '401': - # Player Generation - Player( - (x, y), - [self.visible_sprites], - self.obstacle_sprites, - self.visible_sprites, - self.attack_sprites, - self.attackable_sprites, - 'warrior', - player_id) - - player_id += 1 - - elif col == '402': - # Player Generation - Player( - (x, y), - [self.visible_sprites], - self.obstacle_sprites, - self.visible_sprites, - self.attack_sprites, - self.attackable_sprites, - 'mage', - player_id) - - player_id += 1 - - else: - # Monster Generation - if col == '390': - monster_name = 'bamboo' - elif col == '391': - monster_name = 'spirit' - elif col == '392': - monster_name = 'raccoon' - elif col == ' 393': - monster_name = 'squid' - - Enemy(monster_name, - (x, y), - [ - self.visible_sprites, - self.attackable_sprites - ], - self.visible_sprites, - self.obstacle_sprites) - - def get_players_enemies(self): - self.player_sprites = [sprite for sprite in self.visible_sprites.sprites( - ) if hasattr(sprite, 'sprite_type') and sprite.sprite_type in ('player')] - - self.enemy_sprites = [sprite for sprite in self.visible_sprites.sprites( - ) if hasattr(sprite, 'sprite_type') and sprite.sprite_type in ('enemy')] - - def get_distance_direction(self): - for player in self.player_sprites: - player.distance_direction_from_enemy = [] - - for enemy in self.enemy_sprites: - enemy.distance_direction_from_player = [] - - for player in self.player_sprites: - player_vector = pygame.math.Vector2(player.rect.center) - for enemy in self.enemy_sprites: - enemy_vector = pygame.math.Vector2(enemy.rect.center) - distance = (player_vector - enemy_vector).magnitude() - - if distance > 0: - direction = (player_vector - enemy_vector).normalize() - else: - direction = pygame.math.Vector2() - - enemy.distance_direction_from_player.append( - (distance, direction, player)) - player.distance_direction_from_enemy.append( - (distance, -direction, enemy)) - - def apply_damage_to_player(self): - for enemy in self.enemy_sprites: - for distance, _, player in enemy.distance_direction_from_player: - if distance < enemy.stats.attack_radius and player._input.combat.vulnerable: - player.stats.health -= enemy.stats.attack - player._input.combat.vulnerable = False - player._input.combat.hurt_time = pygame.time.get_ticks() - - def toggle_menu(self): - self.game_paused = not self.game_paused - - def run(self, who='observer'): - # Draw the game - if who == 'observer': - self.visible_sprites.custom_draw(self.observer) - self.ui.display(self.observer) - else: - self.visible_sprites.custom_draw(self.player) - self.ui.display(self.aaa) - - debug('v0.8') - - if not self.game_paused: - # Update the game - for player in self.player_sprites: - player.attack_logic() - - self.get_players_enemies() - self.get_distance_direction() - self.visible_sprites.update() - self.apply_damage_to_player() - - else: - debug('PAUSED') - - for player in self.player_sprites: - if player.is_dead(): - self.dead_players[player.player_id] = True - - self.done = True if self.dead_players.all() == 1 else False diff --git a/multi-agent.py b/multi-agent.py deleted file mode 100644 index 37c5d53..0000000 --- a/multi-agent.py +++ /dev/null @@ -1,83 +0,0 @@ -import random -import torch as T -import numpy as np -import matplotlib.pyplot as plt - -from game import Game -from tqdm import tqdm - -from os import environ -environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' - - -random.seed(1) -np.random.seed(1) -T.manual_seed(1) - -n_episodes = 2000 -game_len = 5000 - -figure_file = 'plots/scores_mp.png' - -game = Game() - -agent_list = [0 for _ in range(game.max_num_players)] - -score_history = np.zeros( - shape=(game.max_num_players, n_episodes)) -best_score = np.zeros(game.max_num_players) -avg_score = np.zeros(game.max_num_players) - -for i in tqdm(range(n_episodes)): - # TODO: Make game.level.reset_map() so we don't __init__ everything all the time (such a waste) - if i != 0: - game.level.__init__(reset=True) - # TODO: Make game.level.reset_map() so we don't pull out and load the agent every time (There is -definitevly- a better way) - for player in game.level.player_sprites: - - player.stats.exp = score_history[player.player_id][i-1] - for agent in agent_list: - player.agent = agent_list[player.player_id] - - agent_list = [0 for _ in range(game.max_num_players)] - - for j in range(game_len): - if not game.level.done: - - game.run() - game.calc_score() - - for player in game.level.player_sprites: - if player.is_dead(): - agent_list[player.player_id] = player.agent - player.kill() - - # if (j == game_len-1 or game.level.done) and game.level.enemy_sprites != []: - # for player in game.level.player_sprites: - # for enemy in game.level.enemy_sprites: - # player.stats.exp *= .95 - - for player in game.level.player_sprites: - if not player.is_dead(): - agent_list[player.player_id] = player.agent - exp_points = player.stats.exp - score_history[player.player_id][i] = exp_points - avg_score[player.player_id] = np.mean( - score_history[player.player_id]) - if avg_score[player.player_id] > best_score[player.player_id]: - best_score[player.player_id] = avg_score[player.player_id] - print(f"Saving models for agent {player.player_id}...") - player.agent.save_models( - actr_chkpt=f"player_{player.player_id}_actor", crtc_chkpt=f"player_{player.player_id}_critic") - print("Models saved ...\n") - - print( - f"\nCumulative score for player {player.player_id}: {score_history[0][i]}\nAverage score for player {player.player_id}: {avg_score[player.player_id]}\nBest score for player {player.player_id}: {best_score[player.player_id]}") - - -plt.plot(score_history) -plt.savefig(figure_file) - -game.quit() - -plt.show() diff --git a/plots/score_sp.png b/plots/score_sp.png deleted file mode 100644 index 98c3bda..0000000 Binary files a/plots/score_sp.png and /dev/null differ diff --git a/pneuma.py b/pneuma.py new file mode 100644 index 0000000..cabc0ef --- /dev/null +++ b/pneuma.py @@ -0,0 +1,264 @@ +import os +import argparse +import torch as T +import numpy as np +import matplotlib.pyplot as plt + +from tqdm import tqdm + +from game import Game + + +if __name__ == "__main__": + + # Create parser + parser = argparse.ArgumentParser( + prog='Pneuma', + description='A Reinforcement Learning platform made with PyGame' + ) + + # Add args + parser.add_argument('--no_seed', + default=False, + action="store_true", + help="Set to True to run without a seed.") + + parser.add_argument('--seed', + type=int, + default=1, + help="The seed for the RNG.") + + parser.add_argument('--n_episodes', + type=int, + default=300, + help="Number of episodes.") + + parser.add_argument('--ep_length', + type=int, + default=5000, + help="Length of each episode.") + + parser.add_argument('--n_players', + type=int, + default=1, + help="Number of players.") + + parser.add_argument('--chkpt_path', + type=str, + default="agents/saved_models", + help="Save/load location for agent models.") + + parser.add_argument('--figure_path', + type=str, + default="figures", + help="Save location for figures.") + + parser.add_argument('--horizon', + type=int, + default=200, + help="The number of steps per update") + + parser.add_argument('--show_pg', + default=False, + action="store_true", + help="Set to True to open PyGame window on desktop") + + parser.add_argument('--no_load', + default=False, + action="store_true", + help="Set to True to ignore saved models") + + parser.add_argument('--gamma', + type=float, + default=0.99, + help="The gamma parameter for PPO") + + parser.add_argument('--alpha', + type=float, + default=0.0003, + help="The alpha parameter for PPO") + + parser.add_argument('--policy_clip', + type=float, + default=0.2, + help="The policy clip") + + parser.add_argument('--batch_size', + type=int, + default=64, + help="The size of each batch") + + parser.add_argument('--n_epochs', + type=int, + default=10, + help="The number of epochs") + + parser.add_argument('--gae_lambda', + type=float, + default=0.95, + help="The lambda parameter of the GAE") + + args = parser.parse_args() + + np.random.seed(args.seed) + T.manual_seed(args.seed) + + n_episodes = args.n_episodes + episode_length = args.ep_length + n_players = args.n_players + + chkpt_path = args.chkpt_path + figure_folder = args.figure_path + + horizon = args.horizon + learnings_per_episode = int(episode_length/horizon) + learn_iters = 0 + + show_pygame = args.show_pg + + # Setup AI stuff + score_history = np.zeros(shape=(n_players, n_episodes)) + best_score = np.zeros(n_players) + + actor_loss = np.zeros(shape=(n_players, + n_episodes)) + + critic_loss = np.zeros(shape=(n_players, + n_episodes)) + + total_loss = np.zeros(shape=(n_players, + n_episodes)) + + game = Game(show_pg=show_pygame, n_players=n_players) + + print("Initializing agents ...") + for player in game.level.player_sprites: + player.setup_agent( + gamma=args.gamma, + alpha=args.alpha, + policy_clip=args.policy_clip, + batch_size=args.batch_size, + N=args.horizon, + n_epochs=args.n_epochs, + gae_lambda=args.gae_lambda, + chkpt_dir=chkpt_path, + no_load=args.no_load + ) + + # Episodes start + for episode in tqdm(range(n_episodes), + dynamic_ncols=True): + + # This handles agent continuity, as well as score persistence + game.level.reset() + + episode_actor_loss = np.zeros( + shape=(n_players, learnings_per_episode)) + + episode_critic_loss = np.zeros( + shape=(n_players, learnings_per_episode)) + + episode_total_loss = np.zeros( + shape=(n_players, learnings_per_episode)) + + # Main game loop + for step in tqdm(range(episode_length), + leave=False, + ascii=True, + dynamic_ncols=True): + + if not game.level.done: + game.run() + if step % horizon == 0: + for player in game.level.player_sprites: + + player.agent.learn() + + episode_actor_loss[player.player_id][learn_iters % learnings_per_episode]\ + = player.agent.actor_loss + + episode_critic_loss[player.player_id][learn_iters % learnings_per_episode]\ + = player.agent.critic_loss + + episode_total_loss[player.player_id][learn_iters % learnings_per_episode]\ + = player.agent.total_loss + + learn_iters += 1 + + # Gather information about the episode + for player in game.level.player_sprites: + + # Update score + score_history[player.player_id][episode] = player.stats.exp + + # Update actor/critic loss + actor_loss[player.player_id][episode] = np.mean( + episode_actor_loss) + + critic_loss[player.player_id][episode] = np.mean( + episode_critic_loss) + + total_loss[player.player_id][episode] = np.mean( + episode_total_loss) + + # Check for new best score + if player.stats.exp > best_score[player.player_id]: + print(f"\nNew best score:\t {player.stats.exp}\ + \nOld best score: \t {best_score[player.player_id]}") + + best_score[player.player_id] = player.stats.exp + + print(f"Saving models for player {player.player_id}...") + + # Save models + player.agent.save_models( + f"A{player.player_id}", + f"C{player.player_id}") + + print(f"Models saved to {chkpt_path}") + + else: + print(f"\nScore this round for player {player.player_id}:\ + {player.stats.exp}") + + # End of training session + print("End of episodes.\ + \n Exiting game...") + game.quit() + + plt.figure() + plt.title("Player Performance") + plt.xlabel("Episode") + plt.ylabel("Score") + plt.legend([f"Player {num}" for num in range(n_players)]) + for player_score in score_history: + plt.plot(player_score) + plt.savefig(f"{figure_folder}/score.png") + + plt.figure() + plt.suptitle("Actor Loss") + plt.xlabel("Episode") + plt.ylabel("Loss") + plt.legend([f"Agent {num}" for num in range(n_players)]) + for actor in actor_loss: + plt.plot(actor) + plt.savefig(f"{figure_folder}/actor_loss.png") + + plt.figure() + plt.suptitle("Critic Loss") + plt.xlabel("Episode") + plt.ylabel("Loss") + plt.legend([f"Agent {num}" for num in range(n_players)]) + for critic in critic_loss: + plt.plot(critic) + plt.savefig(f"{figure_folder}/critic_loss.png") + + plt.figure() + plt.suptitle("Total Loss") + plt.xlabel("Episode") + plt.ylabel("Loss") + plt.legend([f"Agent {num}" for num in range(n_players)]) + for total in total_loss: + plt.plot(total) + plt.savefig(f"{figure_folder}/total_loss.png") + plt.show() diff --git a/single-agent.py b/single-agent.py deleted file mode 100644 index 49441f8..0000000 --- a/single-agent.py +++ /dev/null @@ -1,85 +0,0 @@ -import torch as T -import numpy as np -import matplotlib.pyplot as plt - -from game import Game -from tqdm import tqdm - -from os import environ -environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' - - -np.random.seed(1) -T.manual_seed(1) - -n_episodes = 300 -game_len = 10000 -n_players = 8 - -figure_file = 'plots/score_sp.png' - -game = Game(n_players) - -agent = game.level.player_sprites[0].agent - -score_history = np.zeros(shape=(game.max_num_players, n_episodes)) -best_score = 0 -avg_score = np.zeros(n_episodes) - -for i in tqdm(range(n_episodes)): - # TODO: Make game.level.reset_map() so we don't __init__ everything all the time (such a waste) - if i != 0: - game.level.__init__(n_players, reset=True) - # TODO: Make game.level.reset_map() so we don't pull out and load the agent every time (There is -definitevly- a better way) - - for player in game.level.player_sprites: - player.stats.exp = score_history[player.player_id][i-1] - player.agent = agent - - for j in tqdm(range(game_len)): - if not game.level.done: - - game.run() - game.calc_score() - - for player in game.level.player_sprites: - if player.is_dead(): - player.kill() - - # if (j == game_len-1 or game.level.done) and game.level.enemy_sprites != []: - # for player in game.level.player_sprites: - # for enemy in game.level.enemy_sprites: - # player.stats.exp *= .95 - - for player in game.level.player_sprites: - exp_points = player.stats.exp - score_history[player.player_id][i] = exp_points - avg_score[i] = np.mean(score_history) - - if avg_score[i] >= best_score: - print( - f"\nNew Best score: {avg_score[i]}\ - \nOld Best score: {best_score}" - ) - best_score = avg_score[i] - print("Saving models for agent...") - agent.save_models( - actr_chkpt="player_actor", crtc_chkpt="player_critic") - print("Models saved ...\n") - else: - print( - f"Average score of round: {avg_score[i]}\ - \nBest score: {np.mean(best_score)}" - ) - - -print("\nEpisodes done, saving models...") -agent.save_models( - actr_chkpt="player_actor", crtc_chkpt="player_critic") -print("Models saved ...\n") - -plt.plot(avg_score) -plt.savefig(figure_file) -game.quit() - -plt.show() diff --git a/tmp/ppo/player_actor b/tmp/ppo/player_actor deleted file mode 100644 index 9e4e887..0000000 Binary files a/tmp/ppo/player_actor and /dev/null differ diff --git a/tmp/ppo/player_critic b/tmp/ppo/player_critic deleted file mode 100644 index 3643a59..0000000 Binary files a/tmp/ppo/player_critic and /dev/null differ diff --git a/utils/.settings.py.kate-swp b/utils/.settings.py.kate-swp deleted file mode 100644 index 3a44f1d..0000000 Binary files a/utils/.settings.py.kate-swp and /dev/null differ diff --git a/utils/debug.py b/utils/debug.py index 1233989..6cdc42c 100644 --- a/utils/debug.py +++ b/utils/debug.py @@ -2,11 +2,12 @@ import pygame pygame.init() -font = pygame.font.Font(None,30) +font = pygame.font.Font(None, 30) -def debug(info, y =10, x = 10): + +def debug(info, y=10, x=10): display_surface = pygame.display.get_surface() debug_surf = font.render(str(info), True, 'White') - debug_rect = debug_surf.get_rect(topleft = (x,y)) + debug_rect = debug_surf.get_rect(topleft=(x, y)) pygame.draw.rect(display_surface, 'Black', debug_rect) display_surface.blit(debug_surf, debug_rect) diff --git a/utils/resource_loader.py b/utils/resource_loader.py index 6436d75..21c406f 100644 --- a/utils/resource_loader.py +++ b/utils/resource_loader.py @@ -1,9 +1,14 @@ import pygame from csv import reader -from os import walk +import os def import_csv_layout(path): + script_dir = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(script_dir, + '..', + 'assets', + path) terrain_map = [] with open(path) as level_map: layout = reader(level_map, delimiter=',') @@ -13,11 +18,25 @@ def import_csv_layout(path): def import_folder(path): + script_dir = os.path.dirname(os.path.abspath(__file__)) + + path = os.path.join(script_dir, + '..', + 'assets', + path) surface_list = [] - for _, __, img_files in walk(path): + for _, __, img_files in os.walk(path): for image in img_files: - full_path = f"{path}/{image}" + full_path = os.path.join(path, image) image_surf = pygame.image.load(full_path).convert_alpha() + surface_list.append(image_surf) return surface_list + + +def import_assets(path): + script_dir = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(script_dir, + '..', + 'assets', path)