From a4c78c800f820bf42d3762ea9b8033bd4f96b8ea Mon Sep 17 00:00:00 2001 From: John Mumm Date: Sun, 26 Mar 2017 18:19:45 -0400 Subject: [PATCH] 0.1.0 --- .gitignore | 7 + LICENSE | 674 ++++++++++++++++++ README.md | 121 ++++ acolyte.pony | 40 ++ images/.gitignore | 1 + images/screenshot.png | Bin 0 -> 56314 bytes src/agents/act.pony | 2 + src/agents/agent-data.pony | 256 +++++++ src/agents/agent.pony | 40 ++ src/agents/agents.pony | 37 + src/agents/brigand.pony | 31 + src/agents/cloaked-shadow.pony | 31 + src/agents/ekek.pony | 31 + src/agents/goblin.pony | 31 + src/agents/hellhound.pony | 31 + src/agents/horror.pony | 31 + src/agents/mantis.pony | 31 + src/agents/ooze.pony | 73 ++ src/agents/raven.pony | 33 + src/agents/self.pony | 283 ++++++++ src/agents/skeleton.pony | 31 + src/agents/vampire.pony | 31 + src/ai/combat/adjacent-attack.pony | 22 + src/ai/combat/combat-ai.pony | 18 + src/ai/movement/ai-movement.pony | 13 + src/ai/movement/chase-line-of-sight.pony | 41 ++ src/ai/movement/chase.pony | 31 + src/ai/movement/random-walk.pony | 10 + src/ai/movement/run.pony | 31 + src/ai/strategy/find-next-ai.pony | 33 + src/ai/strategy/replicate.pony | 41 ++ src/datast/iterators.pony | 24 + src/datast/matrix.pony | 49 ++ src/datast/min-heap.pony | 77 ++ src/datast/position.pony | 91 +++ src/datast/queue.pony | 211 ++++++ src/datast/ranged-array.pony | 34 + src/datast/room-shape.pony | 351 +++++++++ src/datast/scan.pony | 73 ++ src/datast/test.pony | 89 +++ src/display/acolyte-display-adapter.pony | 43 ++ src/display/colors.pony | 62 ++ src/display/display-adapter.pony | 5 + src/display/display.pony | 350 +++++++++ src/display/ncurses/ncurses.pony | 60 ++ src/encounters/per-room-agent-placements.pony | 118 +++ .../per-room-initial-placements.pony | 242 +++++++ src/game/game.pony | 321 +++++++++ src/game/turn-manager.pony | 126 ++++ src/generators/diamond-square.pony | 136 ++++ src/generators/digout.pony | 180 +++++ src/generators/simple-tiles.pony | 8 + src/guid/guid.pony | 11 + src/help/help.pony | 31 + src/input/commands.pony | 56 ++ src/input/input.pony | 74 ++ src/input/key-translator.pony | 31 + src/input/keys.pony | 9 + src/invariant/invariant.pony | 41 ++ src/inventory/equipment.pony | 483 +++++++++++++ src/inventory/inventory.pony | 410 +++++++++++ src/log/logger.pony | 3 + src/rand/rand.pony | 42 ++ src/rules/rules.pony | 46 ++ src/world/directions.pony | 19 + src/world/dungeon.pony | 95 +++ src/world/landmark.pony | 41 ++ src/world/line-of-sight.pony | 124 ++++ src/world/looker.pony | 86 +++ src/world/map_viewer.pony | 59 ++ src/world/occupant-codes.pony | 15 + src/world/occupant.pony | 12 + src/world/overworld.pony | 101 +++ src/world/room.pony | 14 + src/world/simple-dungeon.pony | 83 +++ src/world/terrain.pony | 40 ++ src/world/test.pony | 32 + src/world/tile.pony | 228 ++++++ src/world/tiles.pony | 109 +++ src/world/world-builder.pony | 14 + src/world/world.pony | 238 +++++++ 81 files changed, 7283 insertions(+) create mode 100755 .gitignore create mode 100755 LICENSE create mode 100755 README.md create mode 100755 acolyte.pony create mode 100755 images/.gitignore create mode 100755 images/screenshot.png create mode 100755 src/agents/act.pony create mode 100755 src/agents/agent-data.pony create mode 100755 src/agents/agent.pony create mode 100755 src/agents/agents.pony create mode 100755 src/agents/brigand.pony create mode 100755 src/agents/cloaked-shadow.pony create mode 100755 src/agents/ekek.pony create mode 100755 src/agents/goblin.pony create mode 100755 src/agents/hellhound.pony create mode 100755 src/agents/horror.pony create mode 100755 src/agents/mantis.pony create mode 100755 src/agents/ooze.pony create mode 100755 src/agents/raven.pony create mode 100755 src/agents/self.pony create mode 100755 src/agents/skeleton.pony create mode 100755 src/agents/vampire.pony create mode 100755 src/ai/combat/adjacent-attack.pony create mode 100755 src/ai/combat/combat-ai.pony create mode 100755 src/ai/movement/ai-movement.pony create mode 100755 src/ai/movement/chase-line-of-sight.pony create mode 100755 src/ai/movement/chase.pony create mode 100755 src/ai/movement/random-walk.pony create mode 100755 src/ai/movement/run.pony create mode 100755 src/ai/strategy/find-next-ai.pony create mode 100755 src/ai/strategy/replicate.pony create mode 100755 src/datast/iterators.pony create mode 100755 src/datast/matrix.pony create mode 100755 src/datast/min-heap.pony create mode 100755 src/datast/position.pony create mode 100755 src/datast/queue.pony create mode 100755 src/datast/ranged-array.pony create mode 100755 src/datast/room-shape.pony create mode 100755 src/datast/scan.pony create mode 100755 src/datast/test.pony create mode 100755 src/display/acolyte-display-adapter.pony create mode 100755 src/display/colors.pony create mode 100755 src/display/display-adapter.pony create mode 100755 src/display/display.pony create mode 100755 src/display/ncurses/ncurses.pony create mode 100755 src/encounters/per-room-agent-placements.pony create mode 100755 src/encounters/per-room-initial-placements.pony create mode 100755 src/game/game.pony create mode 100755 src/game/turn-manager.pony create mode 100755 src/generators/diamond-square.pony create mode 100755 src/generators/digout.pony create mode 100755 src/generators/simple-tiles.pony create mode 100755 src/guid/guid.pony create mode 100755 src/help/help.pony create mode 100755 src/input/commands.pony create mode 100755 src/input/input.pony create mode 100755 src/input/key-translator.pony create mode 100755 src/input/keys.pony create mode 100755 src/invariant/invariant.pony create mode 100755 src/inventory/equipment.pony create mode 100755 src/inventory/inventory.pony create mode 100755 src/log/logger.pony create mode 100755 src/rand/rand.pony create mode 100755 src/rules/rules.pony create mode 100755 src/world/directions.pony create mode 100755 src/world/dungeon.pony create mode 100755 src/world/landmark.pony create mode 100755 src/world/line-of-sight.pony create mode 100755 src/world/looker.pony create mode 100755 src/world/map_viewer.pony create mode 100755 src/world/occupant-codes.pony create mode 100755 src/world/occupant.pony create mode 100755 src/world/overworld.pony create mode 100755 src/world/room.pony create mode 100755 src/world/simple-dungeon.pony create mode 100755 src/world/terrain.pony create mode 100755 src/world/test.pony create mode 100755 src/world/tile.pony create mode 100755 src/world/tiles.pony create mode 100755 src/world/world-builder.pony create mode 100755 src/world/world.pony diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..914407c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +acolyte +acolyte-dev +.idea/ +*.iml +*.dSYM +.deps/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..09856d6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2017 John Mumm + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100755 index 0000000..f1a2065 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Acolyte + +A procedurally generated RPG inspired by Rogue and written in Pony. + +This is a work in progress but you can currently play complete games. +Because everything is procedurally generated, it's a different game +every time. + +* [Acolyte Instructions](#acolyte-instructions) +* [Installation](#installation) +* [Running](#running) + +![Acolyte](/images/screenshot.png?raw=true "Acolyte") + +## Acolyte Instructions + +Acolyte is played from the terminal. + +The object of the game is to find the Staff of Eternity. + +### Commands + +``` +NORMAL MODE (moving around map): + - movement / attack (collide with enemy) +h - (h)elp +. - wait (turn passes without action) +i - enter INVENTORY MODE +l - enter LOOK MODE (inspect tiles from a distance) +v - enter VIEW MODE (jump around map) +t - (t)ake item on tile +> - descend stairs +< - ascend stairs + - inspect tile you're on (and see item type) +q - quit + +LOOK MODE (inspect objects around map): + - move look cursor +enter - look at highlighted tile +l - return to NORMAL MODE + - return to NORMAL MODE + +VIEW MODE (rapidly look around map): + - jump view by partial screen +v - return to NORMAL MODE + - return to NORMAL MODE + +INVENTORY MODE: + - move through items +enter - equip weapon or armor / drink potion / use misc item +l - (l)ook at item +d - (d)rop item +i - return to NORMAL MODE + - return to NORMAL MODE +``` + +### Objects + +``` +@ - the acolyte +[a-z,A-Z] - beings of all kinds +# - wall +% - weapon or armor +! - potion +$ - cold, hard cash +> - descending stairs +< - ascending stairs +? - who knows! +``` + +## Installation + +Currently, Acolyte is only supported on OSX. + +### Building on Mac OS X + +#### Building ponyc +You'll need llvm 3.7.1 or 3.8.1 and the pcre2 library to build Pony. You can use either homebrew or MacPorts to install these dependencies. + +##### Get Dependencies via Homebrew +Installation via [homebrew](http://brew.sh): +``` +$ brew update +$ brew install homebrew/versions/llvm38 pcre2 libressl +``` + +##### Get Dependencies via MacPorts +Installation via [MacPorts](https://www.macports.org): +``` +$ sudo port install llvm-3.8 pcre2 libressl +$ sudo port select --set llvm mp-llvm-3.8 +``` + +##### Install compiler +Clone the ponyc repo and install the compiler (Acolyte is tested with ponyc +v0.11.1): +``` +git clone https://github.com/ponylang/ponyc +cd ponyc +git checkout 0.11.1 +make config=release install +``` + +#### Building Acolyte +``` +git clone https://github.com/jtfmumm/acolyte +cd acolyte +ponyc +chmod +x acolyte +``` + +## Running +Your terminal must have dimensions of at least 99x31 to run the game properly. + +Assuming you've set execute permissions (e.g. via `chmod +x acolyte`), you can run the game with the following command: +``` +./acolyte +``` + + + diff --git a/acolyte.pony b/acolyte.pony new file mode 100755 index 0000000..8c37e52 --- /dev/null +++ b/acolyte.pony @@ -0,0 +1,40 @@ +use "options" +use "time" +use "src/game" +use "src/input" + +actor Main + new create(env: Env) => + let options = Options(env.args) + let seed = Time.micros() + var noscreen = false + var is_overworld = false + var see_input = false + var is_simple_dungeon = false + var enable_fast = false + + options + .add("overworld", "o", None) + .add("simple-dungeon", "s", None) + .add("seekeys", "k", None) + .add("noscreen", "n", None) + .add("enable-fast", "f", None) + + for option in options do + match option + | ("noscreen", None) => + noscreen = true + | ("overworld", None) => + is_overworld = true + | ("simple-dungeon", None) => + is_simple_dungeon = true + | ("seekeys", None) => + see_input = true + | ("enable-fast", None) => + enable_fast = true + end + end + + Game(env, seed where noscreen = noscreen, + is_overworld = is_overworld, is_simple_dungeon = is_simple_dungeon, + see_input = see_input, enable_fast = enable_fast) diff --git a/images/.gitignore b/images/.gitignore new file mode 100755 index 0000000..e43b0f9 --- /dev/null +++ b/images/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/images/screenshot.png b/images/screenshot.png new file mode 100755 index 0000000000000000000000000000000000000000..47df8aab119070a858f271bbdade57aca8221197 GIT binary patch literal 56314 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 B{1yNJ literal 0 HcmV?d00001 diff --git a/src/agents/act.pony b/src/agents/act.pony new file mode 100755 index 0000000..1d8d166 --- /dev/null +++ b/src/agents/act.pony @@ -0,0 +1,2 @@ +class EmptyAct + fun apply() => None \ No newline at end of file diff --git a/src/agents/agent-data.pony b/src/agents/agent-data.pony new file mode 100755 index 0000000..8080cb4 --- /dev/null +++ b/src/agents/agent-data.pony @@ -0,0 +1,256 @@ +use "collections" +use "time" +use "../ai/movement" +use "../ai/combat" +use "../ai/strategy" +use "../datast" +use "../display" +use "../game" +use "../guid" +use "../log" +use "../rand" +use "../world" + +class Stats + var hp: I32 + var dmg: I32 + var hit_bonus: I32 + var ac: I32 + var gp: I32 + var xp: I32 + var level: I32 + let max_hp: I32 + let next_level_xp: I32 + let depth: I32 + let turn: I32 + + new create(hp': I32, dmg': I32, hit_bonus': I32, ac': I32, gp': I32, + xp': I32, level': I32, max_hp': I32, next_level_xp': I32, depth': I32, + turn': I32) + => + hp = hp' + dmg = dmg' + hit_bonus = hit_bonus' + ac = ac' + gp = gp' + xp = xp' + level = level' + max_hp = max_hp' + next_level_xp = next_level_xp' + depth = depth' + turn = turn' + + fun string(): String => + "HP: " + hp.string() + "<" + max_hp.string() + ">" + " AC: " + + ac.string() + " DMG: " + dmg.string() + " HIT BONUS: " + + hit_bonus.string() + " GP: " + gp.string() + + "\nLEVEL: " + level.string() + " XP: " + xp.string() + + "<" + next_level_xp.string() + "> DEPTH: " + depth.string() + + " TURN: " + turn.string() + +class AgentData + let _agent: Agent tag + // TODO: This should be based on a determinate seed + let _id: U128 = GuidGenerator()() + let _code: I32 + let _movement_ai: MovementAi val + let _combat_ai: CombatAi val + let _find_next_ai: FindNextAi + let _turn_manager: TurnManager tag + let _rand: Rand + var _world: World tag + var _turn_rank: USize = 0 + var _next_act: {()} = EmptyAct + var _dead: Bool = false + // Last known Self position + var _self_pos: Pos val = Pos(-1 , -1) + + var _name: String + var _description: String + var _vision: I32 + var _pos: Pos val + var _ac: I32 + var _hp: I32 + var _dmg: I32 + var _dmg_bonus: I32 = 0 + var _hit_bonus: I32 + var _gp: I32 = 0 + // How much xp is gained by defeating this agent + var _xp_gained: I32 + + new create( + agent': Agent tag, + code': I32, + movement_ai': MovementAi val, + combat_ai': CombatAi val, + turn_manager': TurnManager tag, + rand': Rand, + world': World tag, + name': String, + vision': I32, + pos': Pos val, + ac': I32, + hd': I32, + dmg': I32, + description': String = "", + dmg_bonus': I32 = 0, + hp': I32 = 0, + xp_gained': I32 = 0, + hit_bonus': I32 = 0, + find_next_ai': FindNextAi = BasicFindNextAi + ) + => + _code = code' + _movement_ai = movement_ai' + _combat_ai = combat_ai' + _find_next_ai = find_next_ai' + _turn_manager = turn_manager' + _rand = rand' + _world = world' + + _name = name' + _description = + if description' == "" then "a " + _name + else description' end + _vision = vision' + _pos = pos' + _ac = ac' + if hp' > 0 then + _hp = hp' + else + _hp = rand'.roll(hd', 6) + end + _dmg = dmg' + _dmg_bonus = dmg_bonus' + if xp_gained' > 0 then + _xp_gained = xp_gained' + else + let two: F32 = 2 + _xp_gained = two.pow(hd'.f32() - 1).i32() * 10 + end + _hit_bonus = hit_bonus' + _agent = agent' + _world.add_agent(_agent, _pos, _code) + + fun ref prepare_act(turn_rank: USize, self_pos: Pos val) => + _world.request_submap(_vision, _agent, _pos) + _turn_rank = turn_rank + _self_pos = self_pos + + fun ref deliver_submap(tiles: Tiles iso, display: Display tag) => + _next_act = find_next_act(consume tiles, display) + _turn_manager.ack_ready(_turn_rank, _agent) + + fun move(pos_change: Pos val) => + let target = _pos + pos_change + _world.move_occupant(pos(), target, _agent, _code) + + fun ref update_world(w: World tag) => _world = w + + fun ref update_pos(p: Pos val) => _pos = p + + fun ref take_damage(dmg: I32, attacker: Agent tag, attacker_name: String, + display: Display tag) + => + _hp = _hp - dmg + if _hp <= 0 then + display.log("The " + _name + " is slain by " + attacker_name + "!") + mark_dead() + attacker.modify_xp(_xp_gained) + _turn_manager.report_death(_agent, _pos, _world) + end + _turn_manager.ack() + + fun ref mark_dead() => + _dead = true + + fun confirm_death() => + _turn_manager.ack() + + fun ref find_next_act(tiles: Tiles, display: Display tag): {()} => + _find_next_ai(tiles, this, _self_pos, display) + + fun ref act() => + _next_act() + + fun ref hit(hit_roll: I32, dmg: I32, attacker: Agent tag, + attacker_name: String, display: Display tag) + => + if hit_roll > ac() then + display.log("The " + _name + " takes " + dmg.string() + " damage!") + take_damage(dmg, attacker, attacker_name, display) + else + display.log("The " + attacker_name + " misses the " + _name + "!") + _turn_manager.ack() + end + + fun ref update_hp(h: I32) => _hp = h + + fun ref modify_hp(h: I32, display: Display tag) => + let new_hp = _hp + h + _hp = new_hp + if _hp <= 0 then + display.log("The " + _name + " is slain!") + mark_dead() + end + + fun ref update_dmg(d: I32) => _dmg = d + + fun ref update_dmg_bonus(d: I32) => _dmg_bonus = d + + fun ref modify_ac(a: I32) => _ac = _ac + a + + fun ref modify_gp(g: I32) => _gp = _gp + g + + fun ref modify_hit_bonus(h: I32) => _hit_bonus = _hit_bonus + h + + fun name(): String => _name + + fun description(): String => _description + + fun vision(): I32 => _vision + + fun ref rand(): Rand => _rand + + fun ac(): I32 => _ac + + fun hp(): I32 => _hp + + fun gp(): I32 => _gp + + fun movement_ai(): MovementAi val => _movement_ai + + fun combat_ai(): CombatAi val => _combat_ai + + fun damage(): I32 => _dmg + + fun hit_bonus(): I32 => _hit_bonus + + fun turn_manager(): TurnManager tag => _turn_manager + + fun agent(): Agent tag => _agent + + fun world(): World tag => _world + + fun pos(): Pos val => _pos + + fun code(): I32 => _code + + fun id(): U128 => _id + + fun display_stats(display: Display tag, level: I32, xp: I32, max_hp: I32, + next_level_xp: I32, depth: I32, turn: I32) + => + display.stats(stats(level, xp, max_hp, next_level_xp, depth, turn)) + + fun stats(level: I32, x: I32, max_hp: I32, next_level_xp: I32, + depth: I32, turn: I32): Stats val + => + let h = _hp + let d = _dmg + let hb = _hit_bonus + let a = _ac + let g = _gp + recover + Stats(h, d, hb, a, g, x, level, max_hp, next_level_xp, depth, turn) + end diff --git a/src/agents/agent.pony b/src/agents/agent.pony new file mode 100755 index 0000000..22ad446 --- /dev/null +++ b/src/agents/agent.pony @@ -0,0 +1,40 @@ +use "collections" +use "../datast" +use "../display" +use "../game" +use "../world" + +trait Agent + be act() => data().act() + + be update_world(w: World tag) => data().update_world(w) + + be update_pos(p: Pos val) => data().update_pos(p) + + be prepare_act(turn_rank: USize, self_pos: Pos val) => + data().prepare_act(turn_rank, self_pos) + + be deliver_submap(tiles: Tiles iso, display: Display tag) => + data().deliver_submap(consume tiles, display) + + be hit(hit_roll: I32, dmg: I32, attacker: Agent tag, attacker_name: String, + display: Display tag) + => + data().hit(hit_roll, dmg, attacker, attacker_name, display) + + be describe(display: Display tag) => display.log("You see " + description()) + + be confirm_death() => data().confirm_death() + + be modify_xp(xp: I32) => None + + be modify_hp(hp: I32, display: Display tag) => data().modify_hp(hp, display) + + fun ref _modify_hp(hp: I32, display: Display tag) => data().modify_hp(hp, + display) + + fun ref name(): String => data().name() + + fun ref description(): String => data().description() + + fun ref data(): AgentData diff --git a/src/agents/agents.pony b/src/agents/agents.pony new file mode 100755 index 0000000..30e93c4 --- /dev/null +++ b/src/agents/agents.pony @@ -0,0 +1,37 @@ +use "collections" +use "../datast" +use "../display" +use "../game" +use "../rand" + +class Agents + let _data: Array[Agent tag] = Array[Agent tag] + let _display: Display tag + let _rand: Rand = Rand + var _stopped: Bool = false + + new create(d: Display tag) => + _display = d + + fun ref add(a: Agent tag) => + if not _data.contains(a) then + _data.push(a) + end + + fun ref remove(a: Agent tag) => + try + let idx = _data.find(a) + _data.remove(idx, 1) + end + + fun ref stop() => _stopped = true + fun ref restart() => _stopped = false + + fun ref prepare_act(turn_manager: TurnManager tag, self_pos: Pos val) => + if not _stopped then + try _rand.shuffle_array[Agent tag](_data) end + turn_manager.set_expected_acks(_data.size()) + for (i, agent) in _data.pairs() do + agent.prepare_act(i, self_pos) + end + end diff --git a/src/agents/brigand.pony b/src/agents/brigand.pony new file mode 100755 index 0000000..b5c57f7 --- /dev/null +++ b/src/agents/brigand.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Brigand is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.brigand(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "brigand", + description' = "a brigand", + vision' = 30, + pos' = p, + ac' = 12, + hd' = 2, + dmg' = 6, + hit_bonus' = -1 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/cloaked-shadow.pony b/src/agents/cloaked-shadow.pony new file mode 100755 index 0000000..65d7b7c --- /dev/null +++ b/src/agents/cloaked-shadow.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor CloakedShadow is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.cloaked_shadow(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "cloaked shadow", + description' = "a shadowy figure wrapped in a cloak", + vision' = 30, + pos' = p, + ac' = 14, + hd' = 4, + dmg' = 6, + hit_bonus' = 2 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/ekek.pony b/src/agents/ekek.pony new file mode 100755 index 0000000..857a820 --- /dev/null +++ b/src/agents/ekek.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Ekek is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.ekek(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "ekek", + description' = "a bizarre creature, half human, half bird", + vision' = 30, + pos' = p, + ac' = 11, + hd' = 3, + dmg' = 8, + hit_bonus' = 1 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/goblin.pony b/src/agents/goblin.pony new file mode 100755 index 0000000..897927e --- /dev/null +++ b/src/agents/goblin.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Goblin is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.goblin(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "goblin", + description' = "a goblin", + vision' = 30, + pos' = p, + ac' = 10, + hd' = 1, + dmg' = 4, + hit_bonus' = -2 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/hellhound.pony b/src/agents/hellhound.pony new file mode 100755 index 0000000..b595fdc --- /dev/null +++ b/src/agents/hellhound.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Hellhound is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.hellhound(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "hellhound", + description' = "a fiery hound with relentless red eyes", + vision' = 30, + pos' = p, + ac' = 16, + hd' = 4, + dmg' = 10, + hit_bonus' = 2 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/horror.pony b/src/agents/horror.pony new file mode 100755 index 0000000..a7b9ba1 --- /dev/null +++ b/src/agents/horror.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Horror is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.horror(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "indescribable horror", + description' = "an indescribable horror", + vision' = 30, + pos' = p, + ac' = 17, + hd' = 5, + dmg' = 10, + hit_bonus' = 3 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/mantis.pony b/src/agents/mantis.pony new file mode 100755 index 0000000..ed628c5 --- /dev/null +++ b/src/agents/mantis.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Mantis is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.mantis(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "mantis", + description' = "a gigantic mantis", + vision' = 30, + pos' = p, + ac' = 17, + hd' = 5, + dmg' = 10, + hit_bonus' = 3 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/ooze.pony b/src/agents/ooze.pony new file mode 100755 index 0000000..7efdc52 --- /dev/null +++ b/src/agents/ooze.pony @@ -0,0 +1,73 @@ +use "../ai/combat" +use "../ai/movement" +use "../ai/strategy" +use "../datast" +use "../display" +use "../game" +use "../rand" +use "../world" + +actor Ooze is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.ooze(), + movement_ai' = RandomMovementAi, + combat_ai' = AdjacentAttackCombatAi, + find_next_ai' = OozeFindNext, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "weird ooze", + description' = "a weird ooze spread out across the floor", + vision' = 10, + pos' = p, + ac' = 0, + hd' = 1, + dmg' = 2, + hp' = 1, + xp_gained' = 2 + ) + + fun ref data(): AgentData => _data + +class OozeFindNext + let _scan_close: ScanClose = ScanClose(Pos(0, 0)) + + fun ref apply(tiles: Tiles, data: AgentData, self_pos: Pos val, + display: Display tag): {()} + => + let combat_choice = data.combat_ai()(tiles, data.rand()) + if combat_choice.should_attack then + try + let target = tiles(combat_choice.target).occupant + let hit_roll = data.rand().i32_between(1, 20) + data.hit_bonus() + let d = data.rand().i32_between(1, data.damage()) + {()(target, display, hit_roll, d, data) => + target.hit(hit_roll, d, data.agent(), data.name(), display)} + else + EmptyAct + end + else + (let replicate_target, let should_replicate) = ReplicateAi(tiles, + data.rand()) + if should_replicate and (data.rand().i32_between(1, 18) == 18) then + {()(data, replicate_target) => + let new_ooze = Ooze(replicate_target, data.world(), + data.turn_manager()) + data.world().add_agent_if_empty(new_ooze, replicate_target, + OccupantCodes.ooze()) + data.turn_manager().ack()} + else + let move_target = data.movement_ai()(tiles, data.rand(), self_pos, + _scan_close(self_pos)) + {()(move_target, data) => + data.move(move_target) + data.turn_manager().ack()} + end + end + + diff --git a/src/agents/raven.pony b/src/agents/raven.pony new file mode 100755 index 0000000..93bff0f --- /dev/null +++ b/src/agents/raven.pony @@ -0,0 +1,33 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Raven is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.raven(), + movement_ai' = RandomMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "raven", + description' = "a raven", + vision' = 7, + pos' = p, + ac' = 7, + hd' = 1, + dmg' = 3, + hit_bonus' = -3, + hp' = 2, + xp_gained' = 5 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/self.pony b/src/agents/self.pony new file mode 100755 index 0000000..6e7fe6b --- /dev/null +++ b/src/agents/self.pony @@ -0,0 +1,283 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../display" +use "../game" +use "../input" +use "../inventory" +use "../log" +use "../rand" +use "../world" + +actor Self is Agent + embed _data: AgentData + let _inventory: Inventory = Inventory + let _inventory_manager: InventoryManager + let _game: Game tag + let _display: Display tag + var _next_command: Cmd val = EmptyCmd + // reusable ScanClose + let _scan_close: ScanClose = ScanClose(Pos(0, 0)) + var _next_act: ({()} | None) = None + var _turn_rank: USize = 0 + var _xp: I32 + var _level: I32 + var _next_level_xp: I32 + var _max_hp: I32 = 20 + var _fast_cmd: Cmd val = EmptyCmd + var _fast_mode: Bool = false + var _facing: Pos val = Directions.up() + var _depth: I32 = 0 + var _turn: I32 = 0 + + new create(t_manager: TurnManager tag, display: Display tag, + game: Game tag, level: I32 = 1, xp: I32 = 0, next_level_xp: I32 = 100) => + _display = display + _game = game + _level = level + _xp = xp + _next_level_xp = next_level_xp + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.self(), + movement_ai' = EmptyMovementAi, + combat_ai' = EmptyCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = EmptyWorld, + name' = "acolyte", + description' = "our hero", + vision' = 5, + pos' = Pos(0, 0), + ac' = 10, + hd' = 1, + hp' = _max_hp, + dmg' = 1 + ) + _inventory_manager = InventoryManager(_inventory, this, _data, _display) + + be update_next_level_xp(x: I32) => _next_level_xp = x + + fun ref update_depth(depth: I32) => _depth = depth + + be enter_world_at(pos: Pos val, depth: I32) => + update_pos(pos) + update_depth(depth) + _display_stats() + _game.update_focus(pos) + _game.update_world(data().world()) + + be enqueue_command(cmd: Cmd val) => + _next_command = cmd + + be prepare_act(turn_rank: USize, self_pos: Pos val) => + _data.world().request_submap(_data.vision(), this, _data.pos()) + _turn_rank = turn_rank + + be deliver_submap(ts: Tiles iso, display: Display tag) => + let tiles: Tiles = consume ts + if _fast_mode then + let relative_pos = tiles.relative_pos_for(_data.pos()) + try + if not tiles(relative_pos + _facing).is_passable() then + _game.exit_fast_mode() + end + end + for pos in _scan_close(relative_pos) do + try + if tiles(pos).is_interesting() then + _game.exit_fast_mode() + end + end + end + end + find_next_act(tiles) + _data.turn_manager().ack_ready(_turn_rank, this) + + be hit(hit_roll: I32, dmg: I32, attacker: Agent tag, attacker_name: String, + display: Display tag) + => + if hit_roll > _data.ac() then + display.log("The " + attacker_name + " hit you for " + dmg.string() + + " damage!") + _data.take_damage(dmg, attacker, attacker_name, display) + else + display.log("The " + attacker_name + " misses you!") + _data.turn_manager().ack() + end + + be modify_xp(xp: I32) => + _xp = _xp + xp + if _xp >= _next_level_xp then + level_up() + end + + be drink_potion(h: I32) => + _modify_hp(h, _display) + _display_stats() + + fun ref level_up() => + _level = _level + 1 + _max_hp = _max_hp + 10 + _next_level_xp = _next_level_xp * 2 + _data.update_hp(_max_hp) + _data.modify_hit_bonus(1) + + fun ref find_next_act(tiles: Tiles) => + let cmd = + if _fast_mode then + _fast_cmd + else + _next_command + end + match cmd + | LeftCmd => + _check_move(Directions.left(), tiles) + _facing = Directions.left() + ifdef "keys" then _display.log("Left") end + | RightCmd => + _check_move(Directions.right(), tiles) + _facing = Directions.right() + ifdef "keys" then _display.log("Right") end + | UpCmd => + _check_move(Directions.up(), tiles) + _facing = Directions.up() + ifdef "keys" then _display.log("Up") end + | DownCmd => + _check_move(Directions.down(), tiles) + _facing = Directions.down() + ifdef "keys" then _display.log("Down") end + | TakeCmd => + _data.world().try_take(_data.pos(), this) + | EnterCmd => + _data.world().describe_close(_data.pos(), _display) + | UpStairsCmd => + _data.world().climb(_data.pos(), this) + | DownStairsCmd => + _data.world().portal(_data.pos(), this) + | WaitCmd => + _display.log("Wait") + None + | EmptyCmd => + _data.turn_manager().ack() + else + _display.log("Unrecognized Cmd") + end + + fun ref _check_move(pos_change: Pos val, tiles: Tiles) => + let mid = (tiles.w() / 2) + let mid_pos = Pos(mid, mid) + let target = mid_pos + pos_change + try + if tiles(target).is_occupied() then + try + let enemy = tiles(target).occupant + let hit_roll = _data.rand().i32_between(1, 20) + _data.hit_bonus() + let dmg = _data.rand().i32_between(1, _data.damage()) + let that = this + let name = _data.name() + let next_act = + {()(enemy, _display, hit_roll, dmg, that, name) => + enemy.hit(hit_roll, dmg, that, name, _display)} + _next_act = next_act + else + _next_act = EmptyAct + end + else + _next_act = try build_move(pos_change) else EmptyAct end + end + end + + fun build_move(pos_change: Pos val): {()} ? => + let self = this as Agent tag + let pos = _data.pos() + let world = _data.world() + let code = _data.code() + {()(pos, world, code, pos_change, self) => + let target = pos + pos_change + world.move_occupant(pos, target, self, code)} + + be act() => + match _next_act + | let na: {()} => + na() + _next_act = None + else + _data.turn_manager().ack() + end + + be pick_up_item(i: Item val) => + if _inventory_manager.add(i) then + _display.log("You take the " + i.name()) + _data.world().remove_item(_data.pos()) + else + _display.log("There is no space left.\nYou need to drop something.") + end + + be display_stats() => + _display_stats() + + fun ref _display_stats() => + _data.display_stats(_display, _level, _xp, _max_hp, _next_level_xp, _depth, + _turn) + + be process_inventory_command(cmd: Cmd val, display: Display tag) => + match cmd + | LeftCmd => + _inventory_manager.prev() + | RightCmd => + _inventory_manager.next() + | UpCmd => + _inventory_manager.prev() + | DownCmd => + _inventory_manager.next() + | LookCmd => + display.log(_inventory_manager.description()) + | EnterCmd => + _inventory_manager.try_item() + | ECmd => + _inventory_manager.equip() + | UCmd => + _inventory_manager.utilize() + | DropCmd => + _inventory_manager.drop(_data.world(), _data.pos()) + end + display.inventory(_inventory_manager.displayable()) + _display_stats() + + be enter_fast_mode(cmd: Cmd val) => _enter_fast_mode(cmd) + + fun ref _enter_fast_mode(cmd: Cmd val) => + _fast_cmd = cmd + match _fast_cmd + | LeftCmd => + _facing = Directions.left() + | RightCmd => + _facing = Directions.right() + | UpCmd => + _facing = Directions.up() + | DownCmd => + _facing = Directions.down() + end + _fast_mode = true + _next_command = EmptyCmd + + be exit_fast_mode() => _exit_fast_mode() + + fun ref _exit_fast_mode() => + _fast_cmd = EmptyCmd + _fast_mode = false + + be exit_inventory_mode() => _inventory_manager.reset_current() + + be clear_commands() => + _next_command = EmptyCmd + + be increment_turn() => + _turn = _turn + 1 + + be win_game() => _game.win() + + fun ref data(): AgentData => _data + diff --git a/src/agents/skeleton.pony b/src/agents/skeleton.pony new file mode 100755 index 0000000..6fe43a0 --- /dev/null +++ b/src/agents/skeleton.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Skeleton is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.skeleton(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "skeleton", + description' = "a moving skeleton", + vision' = 30, + pos' = p, + ac' = 14, + hd' = 3, + dmg' = 8, + hit_bonus' = 0 + ) + + fun ref data(): AgentData => _data diff --git a/src/agents/vampire.pony b/src/agents/vampire.pony new file mode 100755 index 0000000..d4cfecc --- /dev/null +++ b/src/agents/vampire.pony @@ -0,0 +1,31 @@ +use "../ai/combat" +use "../ai/movement" +use "../datast" +use "../game" +use "../rand" +use "../world" + +actor Vampire is Agent + embed _data: AgentData + + new create(p: Pos val, w: World tag, t_manager: TurnManager tag) => + _data = + AgentData(where + agent' = this, + code' = OccupantCodes.vampire(), + movement_ai' = ChaseLineOfSightMovementAi, + combat_ai' = AdjacentAttackCombatAi, + turn_manager' = t_manager, + rand' = Rand, + world' = w, + name' = "vampire", + description' = "a vampire", + vision' = 30, + pos' = p, + ac' = 20, + hd' = 6, + dmg' = 12, + hit_bonus' = 4 + ) + + fun ref data(): AgentData => _data diff --git a/src/ai/combat/adjacent-attack.pony b/src/ai/combat/adjacent-attack.pony new file mode 100755 index 0000000..eb357c0 --- /dev/null +++ b/src/ai/combat/adjacent-attack.pony @@ -0,0 +1,22 @@ +use "../../datast" +use "../../rand" +use "../../world" + +primitive AdjacentAttackCombatAi + fun apply(tiles: Tiles, rand: Rand): CombatChoice val => + let mid = Pos(tiles.w() / 2, tiles.h() / 2) + try + if tiles(mid + Directions.up()).is_self() then + CombatChoice(mid + Directions.up(), true) + elseif tiles(mid + Directions.right()).is_self() then + CombatChoice(mid + Directions.right(), true) + elseif tiles(mid + Directions.down()).is_self() then + CombatChoice(mid + Directions.down(), true) + elseif tiles(mid + Directions.left()).is_self() then + CombatChoice(mid + Directions.left(), true) + else + CombatChoice(Pos(-1, -1), false) + end + else + CombatChoice(Pos(-1, -1), false) + end diff --git a/src/ai/combat/combat-ai.pony b/src/ai/combat/combat-ai.pony new file mode 100755 index 0000000..469a8ae --- /dev/null +++ b/src/ai/combat/combat-ai.pony @@ -0,0 +1,18 @@ +use "../../datast" +use "../../rand" +use "../../world" + +interface CombatAi + fun apply(tiles: Tiles, rand: Rand): CombatChoice val + +class EmptyCombatAi + fun apply(tiles: Tiles, rand: Rand): CombatChoice val => + CombatChoice(Pos(0, 0), false) + +class CombatChoice + let target: Pos val + let should_attack: Bool + + new val create(t: Pos val, sa: Bool) => + target = t + should_attack = sa diff --git a/src/ai/movement/ai-movement.pony b/src/ai/movement/ai-movement.pony new file mode 100755 index 0000000..6d5013a --- /dev/null +++ b/src/ai/movement/ai-movement.pony @@ -0,0 +1,13 @@ +use "../../datast" +use "../../rand" +use "../../world" + +interface MovementAi + fun apply(tiles: Tiles, rand: Rand, abs_self_pos: Pos val, + scan_close: ScanClose): Pos val + +class EmptyMovementAi + fun apply(tiles: Tiles, rand: Rand, abs_self_pos: Pos val, + scan_close: ScanClose): Pos val + => + Pos(0, 0) diff --git a/src/ai/movement/chase-line-of-sight.pony b/src/ai/movement/chase-line-of-sight.pony new file mode 100755 index 0000000..b80c51c --- /dev/null +++ b/src/ai/movement/chase-line-of-sight.pony @@ -0,0 +1,41 @@ +use "collections" +use "../../datast" +use "../../rand" +use "../../world" + +primitive ChaseLineOfSightMovementAi + fun apply(tiles: Tiles, rand: Rand, abs_self_pos: Pos val, + scan_close: ScanClose): Pos val + => + let self_pos = tiles.relative_pos_for(abs_self_pos) + let mid = Pos(tiles.w() / 2, tiles.h() / 2) + for pos in scan_close(self_pos) do + let line = LineIterator(mid, pos) + for p in line do + try + let t = tiles(p) + if t.is_transparent() then + if t.is_self() then + let dx: I32 = + if p.x < mid.x then -1 + elseif p.x > mid.x then 1 + else 0 end + let dy: I32 = + if p.y < mid.y then -1 + elseif p.y > mid.y then 1 + else 0 end + match (dx, dy) + | (_, 0) => return Pos(dx, dy) + | (0, _) => return Pos(dx, dy) + else + if rand.flip() == 0 then return Pos(dx, 0) + else return Pos(0, dy) end + end + end + else + continue + end + end + end + end + Directions.rand_cardinal() diff --git a/src/ai/movement/chase.pony b/src/ai/movement/chase.pony new file mode 100755 index 0000000..2c1fa97 --- /dev/null +++ b/src/ai/movement/chase.pony @@ -0,0 +1,31 @@ +use "collections" +use "../../datast" +use "../../rand" +use "../../world" + +primitive ChaseMovementAi + fun apply(tiles: Tiles, rand: Rand, abs_self_pos: Pos val, + scan_close: ScanClose): Pos val + => + let self_pos = tiles.relative_pos_for(abs_self_pos) + let x = self_pos.x + let y = self_pos.y + let mid = Pos(tiles.w() / 2, tiles.h() / 2) + if (self_pos >= Pos(0, 0)) or (self_pos < Pos(tiles.w(), tiles.h())) then + let dx: I32 = + if x < mid.x then -1 + elseif x > mid.x then 1 + else 0 end + let dy: I32 = + if y < mid.y then -1 + elseif y > mid.y then 1 + else 0 end + match (dx, dy) + | (_, 0) => return Pos(dx, dy) + | (0, _) => return Pos(dx, dy) + else + if rand.flip() == 0 then return Pos(dx, 0) + else return Pos(0, dy) end + end + end + Directions.rand_cardinal() diff --git a/src/ai/movement/random-walk.pony b/src/ai/movement/random-walk.pony new file mode 100755 index 0000000..b29a81d --- /dev/null +++ b/src/ai/movement/random-walk.pony @@ -0,0 +1,10 @@ +use "collections" +use "../../datast" +use "../../rand" +use "../../world" + +primitive RandomMovementAi + fun apply(tiles: Tiles, rand: Rand, self_pos: Pos val, + scan_close: ScanClose): Pos val + => + Directions.rand_cardinal() diff --git a/src/ai/movement/run.pony b/src/ai/movement/run.pony new file mode 100755 index 0000000..0c43760 --- /dev/null +++ b/src/ai/movement/run.pony @@ -0,0 +1,31 @@ +use "collections" +use "../../datast" +use "../../rand" +use "../../world" + +primitive RunMovementAi + fun apply(tiles: Tiles, rand: Rand, abs_self_pos: Pos val, + scan_close: ScanClose): Pos val + => + let self_pos = tiles.relative_pos_for(abs_self_pos) + let x = self_pos.x + let y = self_pos.y + let mid = Pos(tiles.w() / 2, tiles.h() / 2) + if (self_pos >= Pos(0, 0)) or (self_pos < Pos(tiles.w(), tiles.h())) then + let dx: I32 = + if x < mid.x then 1 + elseif x > mid.x then -1 + else 0 end + let dy: I32 = + if y < mid.y then 1 + elseif y > mid.y then -1 + else 0 end + match (dx, dy) + | (_, 0) => return Pos(dx, dy) + | (0, _) => return Pos(dx, dy) + else + if rand.flip() == 0 then return Pos(dx, 0) + else return Pos(0, dy) end + end + end + Directions.rand_cardinal() diff --git a/src/ai/strategy/find-next-ai.pony b/src/ai/strategy/find-next-ai.pony new file mode 100755 index 0000000..0981cdc --- /dev/null +++ b/src/ai/strategy/find-next-ai.pony @@ -0,0 +1,33 @@ +use "../../agents" +use "../../datast" +use "../../display" +use "../../world" + +interface FindNextAi + fun ref apply(tiles: Tiles, data: AgentData, self_pos: Pos val, display: + Display tag): {()} + +class BasicFindNextAi + let _scan_close: ScanClose = ScanClose(Pos(0, 0)) + + fun ref apply(tiles: Tiles, data: AgentData, self_pos: Pos val, display: + Display tag): {()} + => + let combat_choice = data.combat_ai()(tiles, data.rand()) + if combat_choice.should_attack then + try + let target = tiles(combat_choice.target).occupant + let hit_roll = data.rand().i32_between(1, 20) + data.hit_bonus() + let d = data.rand().i32_between(1, data.damage()) + {()(target, display, hit_roll, d, data) => + target.hit(hit_roll, d, data.agent(), data.name(), display)} + else + EmptyAct + end + else + let move_target = data.movement_ai()(tiles, data.rand(), self_pos, + _scan_close(self_pos)) + {()(move_target, data) => + data.move(move_target) + data.turn_manager().ack()} + end diff --git a/src/ai/strategy/replicate.pony b/src/ai/strategy/replicate.pony new file mode 100755 index 0000000..af57439 --- /dev/null +++ b/src/ai/strategy/replicate.pony @@ -0,0 +1,41 @@ +use "collections" +use "../../datast" +use "../../rand" +use "../../world" + +primitive ReplicateAi + fun apply(tiles: Tiles, rand: Rand): (Pos val, Bool) => + let mid = Pos(tiles.w() / 2, tiles.h() / 2) + if in_range(tiles) then + try + if tiles(mid + Directions.up()).is_open() then + ((mid + Directions.up()), true) + elseif tiles(mid + Directions.right()).is_open() then + ((mid + Directions.right()), true) + elseif tiles(mid + Directions.down()).is_open() then + ((mid + Directions.down()), true) + elseif tiles(mid + Directions.left()).is_open() then + ((mid + Directions.left()), true) + else + (Pos(-1, -1), false) + end + else + (Pos(-1, -1), false) + end + else + (Pos(-1, -1), false) + end + + fun in_range(tiles: Tiles): Bool => + for row in Range(0, tiles.h().usize()) do + for col in Range(0, tiles.w().usize()) do + let x = col.i32() + let y = row.i32() + try + if tiles(Pos(x, y)).is_self() then + return true + end + end + end + end + false diff --git a/src/datast/iterators.pony b/src/datast/iterators.pony new file mode 100755 index 0000000..bfe5982 --- /dev/null +++ b/src/datast/iterators.pony @@ -0,0 +1,24 @@ +class Iterators[V: Any val] is Iterator[V] + let _iters: Array[Iterator[V]] + var _idx: USize = 0 + + new create(iters: Array[Iterator[V]]) => + _iters = iters + + fun ref has_next(): Bool => + try + while (_idx < _iters.size()) do + if _iters(_idx).has_next() then return true end + _idx = _idx + 1 + end + false + else + false + end + + fun ref next(): V ? => + let n = _iters(_idx).next() + if not _iters(_idx).has_next() then + _idx = _idx + 1 + end + consume n diff --git a/src/datast/matrix.pony b/src/datast/matrix.pony new file mode 100755 index 0000000..0ca9c03 --- /dev/null +++ b/src/datast/matrix.pony @@ -0,0 +1,49 @@ +use "collections" + +class Matrix[A: Any #read] + let _h: I32 + let _w: I32 + let _data: Array[(A | None)] + + new create(h: I32, w: I32, data_gen: ({(): A^} val | None) = None) => + _h = h + _w = w + let cell_count = (_h * _w).usize() + _data = Array[(A | None)](cell_count) + match data_gen + | let f: {(): A} val => + for cell in Range(0, cell_count) do + _data.push(f()) + end + else + for cell in Range(0, cell_count) do + _data.push(None) + end + end + + fun _to_cell(pos: Pos val): USize => + ((_w * pos.y) + pos.x).usize() + + fun _in_bounds(pos: Pos val): Bool => + (pos.y >= 0) and (pos.y < _h) and + (pos.x >= 0) and (pos.x < _w) + + fun ref apply(pos: Pos val): A ? => + if _in_bounds(pos) then + match _data(_to_cell(pos)) + | let a: A => a + else + @printf[I32](("Matrix Read Error\n").cstring()) + error + end + else + error + end + + fun ref update(pos: Pos val, value: A) ? => + if _in_bounds(pos) then + _data(_to_cell(pos)) = value + else + @printf[I32](("Matrix Write Error: Out of bounds\n").cstring()) + error + end \ No newline at end of file diff --git a/src/datast/min-heap.pony b/src/datast/min-heap.pony new file mode 100755 index 0000000..de1da02 --- /dev/null +++ b/src/datast/min-heap.pony @@ -0,0 +1,77 @@ +class MinHeap[V: (Comparable[V] #read & Any #alias)] + let _data: Array[V] + + new create(len: USize = 0) => + _data = Array[V](len) + + fun ref _left_child_idx(idx: USize): USize => + ((idx + 1) * 2) - 1 + + fun ref _right_child_idx(idx: USize): USize => + ((idx + 1) * 2) + + fun ref _parent_idx(idx: USize): USize => + ((idx + 1) / 2) - 1 + + fun ref _bubble_up(idx: USize) => + if idx == 0 then return end + try + let el = _data(idx) + let parent_idx = _parent_idx(idx) + let parent = _data(parent_idx) + if el < parent then + _data(idx) = parent + _data(parent_idx) = el + _bubble_up(parent_idx) + end + end + + fun ref _bubble_down(idx: USize) => + if idx >= (_data.size() - 1) then return end + let left_idx = _left_child_idx(idx) + let right_idx = _right_child_idx(idx) + if left_idx >= _data.size() then return end + try + let el = _data(idx) + if right_idx >= _data.size() then + let left = _data(left_idx) + if left < el then + _data(idx) = left + _data(left_idx) = el + end + else + let left = _data(left_idx) + let right = _data(right_idx) + if left < right then + if left < el then + _data(idx) = left + _data(left_idx) = el + _bubble_down(left_idx) + end + else + if right < el then + _data(idx) = right + _data(right_idx) = el + _bubble_down(right_idx) + end + end + end + end + + fun ref insert(v: V) => + _data.push(v) + _bubble_up(_data.size() - 1) + + fun ref pop(): V ? => + let el = _data(0) + if _data.size() > 1 then + _data(0) = _data.pop() + _bubble_down(0) + else + _data.pop() + end + el + + fun peek(): this->V ? => _data(0) + + fun size(): USize => _data.size() diff --git a/src/datast/position.pony b/src/datast/position.pony new file mode 100755 index 0000000..82f66b2 --- /dev/null +++ b/src/datast/position.pony @@ -0,0 +1,91 @@ +class Pos is (Equatable[Pos] & Stringable) + let x: I32 + let y: I32 + + new val create(x': I32, y': I32) => + x = x' + y = y' + + fun add(p: Pos val): Pos val => + Pos(x + p.x, y + p.y) + + fun sub(p: Pos val): Pos val => + Pos(x - p.x, y - p.y) + + fun mul(scalar: I32): Pos val => + Pos(scalar * x, scalar * y) + + fun eq(that: box->Pos): Bool => + (x == that.x) and (y == that.y) + + fun gt(that: box->Pos): Bool => + ((x >= that.x) and (y > that.y)) + or ((x > that.x) and (y >= that.y)) + + fun lt(that: box->Pos): Bool => + ((x <= that.x) and (y < that.y)) + or ((x < that.x) and (y <= that.y)) + + fun ge(that: box->Pos): Bool => + gt(that) or eq(that) + + fun le(that: box->Pos): Bool => + lt(that) or eq(that) + + fun string(): String iso^ => + let x_str: String iso = x.string().clone() + let y_str: String iso = y.string().clone() + recover String().>append("Pos(" + consume x_str + ", " + consume y_str + ")") end + +class PosStraightIterator is Iterator[Pos val] + let direction: Pos val + var _x: I32 + var _y: I32 + var _idx: I32 = 0 + var _max: I32 = 0 + var _min: I32 = 0 + var _bias: I32 = 0 + + new create(pos1: Pos val, pos2: Pos val) => + _x = pos1.x + _y = pos1.y + direction = + if _x > pos2.x then + _min = pos2.x + _bias = -1 + _idx = _x + Pos(-1, 0) + elseif _x < pos2.x then + _max = pos2.x + _bias = 1 + _idx = _x + Pos(1, 0) + elseif _y > pos2.y then + _min = pos2.y + _bias = -1 + _idx = _y + Pos(0, -1) + elseif _y < pos2.y then + _max = pos2.y + _bias = 1 + _idx = _y + Pos(0, 1) + else + _max = 0 + _bias = 1 + _idx = 0 + Pos(-1, -1) + end + + fun has_next(): Bool => + if _bias == 1 then + _idx < _max + else + _idx > _min + end + + fun ref next(): Pos val => + _idx = _idx + _bias + _x = _x + direction.x + _y = _y + direction.y + Pos(_x, _y) diff --git a/src/datast/queue.pony b/src/datast/queue.pony new file mode 100755 index 0000000..9fda601 --- /dev/null +++ b/src/datast/queue.pony @@ -0,0 +1,211 @@ +use "assert" +use "collections" +use "debug" + +class Queue[A: Any #alias] + embed _data: Array[A] + var _front_ptr: USize = 0 + var _back_ptr: USize = 0 + var _mod: USize = 0 + var _size: USize = 0 + + new create(len: USize = 0) => + """ + Create a queue. + """ + let n = len.max(2).next_pow2() + _mod = n - 1 + _data = Array[A](n) + _size = 0 + + fun size(): USize => + """ + The size of the queue. + """ + _size + + fun space(): USize => _mod + 1 + + fun apply(i: USize): this->A ? => + """ + Get the i-th element from the front of the queue, + raising an error if the index is out of bounds. + """ + if i < _size then + _data((i + _front_ptr) and _mod) + else + error + end + + fun ref _update(i: USize, value: A): A^ ? => + """ + Change the i-th element, raising an error if the index is out of bounds. + """ + _data(i) = consume value + + fun ref enqueue(a: A) ? => + """ + Add an element to the back of the queue, doubling the allocation + if the queue size has reached the allocated size and shifting any + wrapping elements to the end of a contiguous series. + """ + if _size < (_mod / 2) then + if _data.size() == 0 then + _data.push(consume a) + _back_ptr = _data.size() + elseif _back_ptr >= space() then + _back_ptr = 0 + _data(0) = consume a + elseif _back_ptr >= _data.size() then + _data.push(consume a) + _back_ptr = (_back_ptr + 1) and _mod + else + _data(_back_ptr) = consume a + _back_ptr = (_back_ptr + 1) and _mod + end + else + let new_space = space() * 2 + _data.reserve(new_space) + _mod = new_space - 1 + + if _back_ptr == _data.size() then + _data.push(consume a) + _back_ptr = _data.size() + elseif _front_ptr > _back_ptr then + for i in Range(0, _back_ptr) do + _data.push(_data(i)) + end + _data.push(consume a) + _back_ptr = _data.size() and _mod + else + _data(_back_ptr) = consume a + _back_ptr = _back_ptr + 1 + end + end + _size = _size + 1 + + fun ref dequeue(): A! ? => + if _size > 0 then + let a = _data(_front_ptr) + _front_ptr = (_front_ptr + 1) and _mod + _size = _size - 1 + a + else + error + end + + fun peek(): this->A ? => + if _size > 0 then + _data(_front_ptr) + else + error + end + + fun ref clear(): Queue[A]^ => + """ + Remove all elements from the queue. + The queue is returned to allow call chaining. + """ + _size = 0 + _front_ptr = 0 + _back_ptr = 0 + this + + fun ref clear_n(n: USize) => + if (_size > 0) and (n > 0) then + let to_clear = if _size < (n - 1) then (_size - 1) else n end + _front_ptr = (_front_ptr + to_clear) and _mod + _size = _size - to_clear + end + + fun contains(a: A!, pred: {(box->A!, box->A!): Bool} val = + {(l: box->A!, r: box->A!): Bool => l is r}): Bool => + """ + Returns true if the queue contains `value`, false otherwise. + """ + try + if _front_ptr < _back_ptr then + for i in Range(_front_ptr, _back_ptr) do + if pred(_data(i), a) then return true end + end + return false + else + for i in Range(_front_ptr, _data.size()) do + if pred(_data(i), a) then return true end + end + for i in Range(0, _back_ptr) do + if pred(_data(i), a) then return true end + end + return false + end + else + false + end + + fun values(): QueueValues[A, this->Array[A]]^ => + QueueValues[A, this->Array[A]](_data, _front_ptr, _back_ptr) + + fun pairs(): QueuePairs[A, this->Array[A]]^ => + QueuePairs[A, this->Array[A]](_data, _front_ptr, _back_ptr) + +class QueueValues[A, B: Array[A] #read] is Iterator[B->A] + let _data: B + var _front: USize + var _last_front: USize + let _back: USize + let _initial_front: USize + + new create(data: B, front: USize, back: USize) => + _data = data + _front = front + _last_front = _front + _back = back + _initial_front = front + + fun has_next(): Bool => + if _front >= _last_front then + _front != _back + else + (_back < _data.size()) and (_front != _back) + end + + fun ref next(): B->A ? => + _last_front = _front + _data(_front = (_front + 1) % _data.size()) + + fun ref rewind(): QueueValues[A, B] => + _front = _initial_front + _last_front = _front + this + +class QueuePairs[A, B: Array[A] #read] is Iterator[(USize, B->A)] + let _data: B + var _front: USize + var _last_front: USize + let _back: USize + let _initial_front: USize + + new create(data: B, front: USize, back: USize) => + _data = data + _front = front + _last_front = _front + _back = back + _initial_front = _front + + fun has_next(): Bool => + if _front >= _last_front then + _front != _back + else + (_back < _data.size()) and (_front != _back) + end + + fun ref next(): (USize, B->A) ? => + _last_front = _front + let relative_idx = + if _front >= _initial_front then + _front - _initial_front + else + _front + (_data.size() - _initial_front) + end + (relative_idx, _data(_front = (_front + 1) % _data.size())) + diff --git a/src/datast/ranged-array.pony b/src/datast/ranged-array.pony new file mode 100755 index 0000000..ea4889c --- /dev/null +++ b/src/datast/ranged-array.pony @@ -0,0 +1,34 @@ +use "collections" +use "debug" + +class RangedArray[V: Any #alias] + let _data: Map[ISize, V] = Map[ISize, V] + let _ranges: Array[ISize] = Array[ISize] + var _last_idx: ISize = -1 + var _size: USize = 0 + + fun ref add(v: V, count: USize) => + let next_idx = _last_idx + count.isize() + _data(next_idx) = consume v + _ranges.push(next_idx) + _last_idx = next_idx + _size = _size + count + + fun apply(i: USize): this->V ? => + let ext_idx = i.isize() + for idx in _ranges.values() do + if ext_idx <= idx then return _data(idx) end + end + error + + fun ref append(r: RangedArray[V]) => + let current_max: ISize = try _ranges(_ranges.size() - 1) else 0 end + for range in r._ranges.values() do + try + let new_range = range + current_max + _data(new_range) = r._data(range) + _ranges.push(new_range) + end + end + + fun size(): USize => _size diff --git a/src/datast/room-shape.pony b/src/datast/room-shape.pony new file mode 100755 index 0000000..47c7268 --- /dev/null +++ b/src/datast/room-shape.pony @@ -0,0 +1,351 @@ +use "collections" +use "debug" +use "../rand" + +trait RoomShape + fun perimeter_space(i: USize): Pos val ? + + fun rand_position(): Pos val => + let roll = Rand.usize_between(0, perimeter_size() - 1) + try perimeter_space(roll) else Pos(0, 0) end + + fun rand_interior_position(): Pos val + + fun perimeter(): Iterator[Pos val] => RoomShapePerimeter(this) + + fun interior(): Iterator[Pos val] + + fun all(): Iterator[Pos val] => + let arr: Array[Iterator[Pos val]] = Array[Iterator[Pos val]] + arr.push(perimeter()) + arr.push(interior()) + Iterators[Pos val](consume arr) + + fun perimeter_size(): USize + +class RoomShapePerimeter is Iterator[Pos val] + let _shape: RoomShape box + var _i: USize = 0 + + new create(shape: RoomShape box) => + _shape = shape + + fun has_next(): Bool => + _i < _shape.perimeter_size() + + fun ref next(): Pos val ? => + _shape.perimeter_space(_i = _i + 1) + + fun ref rewind(): RoomShapePerimeter => + _i = 0 + this + +class RectRoom is RoomShape + let _tlx: I32 + let _tly: I32 + let _brx: I32 + let _bry: I32 + let _pt1: I32 + let _pt2: I32 + let _pt3: I32 + let _perimeter_size: USize + + new val create(tl: Pos val, br: Pos val) => + _tlx = tl.x + _tly = tl.y + _brx = br.x + _bry = br.y + _pt1 = _brx - _tlx + _pt2 = _pt1 + ((_bry + 1) - _tly) + _pt3 = _pt2 + (_brx - _tlx) + _perimeter_size = (((_brx - _tlx) * 2) + ((_bry - _tly) * 2)).usize() + + fun perimeter_space(i': USize): Pos val ? => + let i = i'.i32() + if (i < 0) or (i >= _perimeter_size.i32()) then error end + let x = + if i < _pt1 then + i + _tlx + elseif i < _pt2 then + _brx + elseif i < _pt3 then + let diff = (i - _pt2) + 1 + _brx - diff + else + _tlx + end + let y = + if i < (_pt1 + 1) then + _tly + elseif i < _pt2 then + let diff = i - _pt1 + _tly + diff + elseif i < _pt3 then + _bry + else + let diff = (i - _pt3) + 1 + _bry - diff + end + Pos(x, y) + + fun interior(): Iterator[Pos val] => RectInterior(this) + + fun rand_interior_position(): Pos val => + let area = interior_width() * interior_height() + let roll = Rand.i32_between(0, area - 1) + let x = (roll % interior_width()) + interior_starting_pos().x + let y = (roll / interior_width()) + interior_starting_pos().y + Pos(x, y) + + fun perimeter_size(): USize => _perimeter_size + + fun interior_width(): I32 => (_brx - _tlx) - 1 + fun interior_height(): I32 => (_bry - _tly) - 1 + fun interior_starting_pos(): Pos val => Pos(_tlx + 1, _tly + 1) + +class RectInterior is Iterator[Pos val] + let _rect: RectRoom box + var _y: I32 + var _x: I32 + var _height: I32 + var _width: I32 + var _y_idx: I32 = 0 + var _x_idx: I32 = 0 + + new create(rect: RectRoom box) => + _rect = rect + let start_pos = _rect.interior_starting_pos() + _x = start_pos.x + _y = start_pos.y + _height = _rect.interior_height() + _width = _rect.interior_width() + + fun has_next(): Bool => _y_idx < _height + + fun ref next(): Pos val => + let pos = Pos(_x, _y) + if _x_idx < (_width - 1) then + _x = _x + 1 + _x_idx = _x_idx + 1 + else + _y = _y + 1 + _y_idx = _y_idx + 1 + _x = _rect.interior_starting_pos().x + _x_idx = 0 + end + pos + +class DiamondRoom is RoomShape + let _internal: RoomShape val + + new val create(left: Pos val, right: Pos val) => + if ((right.x - left.x) % 2) == 0 then + _internal = OddDiamondRoom(left, right) + else + _internal = EvenDiamondRoom(left, right) + end + + fun perimeter_space(i: USize): Pos val ? => _internal.perimeter_space(i) + fun rand_position(): Pos val => _internal.rand_position() + fun rand_interior_position(): Pos val => _internal.rand_interior_position() + fun perimeter(): Iterator[Pos val]^ => _internal.perimeter() + fun interior(): Iterator[Pos val] => _internal.interior() + fun perimeter_size(): USize => _internal.perimeter_size() + +class OddDiamondRoom is RoomShape + let _lx: I32 + let _ly: I32 + let _rx: I32 + let _ry: I32 + let _radius: I32 + let _perimeter_size: USize + let _pt1: I32 + let _pt2: I32 + let _pt3: I32 + + new val create(left: Pos val, right: Pos val) => + _lx = left.x + _ly = left.y + _rx = right.x + _ry = right.y + let space = (_rx - _lx) - 1 + _radius = (space.f64() / 2).ceil().i32() + _perimeter_size = (_radius * 4).usize() + _pt1 = _radius + _pt2 = _pt1 + _radius + _pt3 = _pt2 + _radius + + fun perimeter_space(i': USize): Pos val ? => + let i = i'.i32() + if (i < 0) or (i >= _perimeter_size.i32()) then error end + let x = + if i <= _pt2 then + i + _lx + else + _rx - (i - _pt2) + end + let y = + if i <= _pt1 then + _ly + i + elseif i <= _pt2 then + (_ly + _radius) - (i - _pt1) + elseif i <= _pt3 then + _ly - (i - _pt2) + else + (_ly - _radius) + (i - _pt3) + end + Pos(x, y) + + fun perimeter_size(): USize => _perimeter_size + + fun interior(): Iterator[Pos val] => OddDiamondInterior(this) + + fun rand_interior_position(): Pos val => + // let area = (_radius * _radius) + ((_radius - 1) * (_radius - 1)) + // let roll = Rand.usize_between(0, area - 1) + let x = ((_rx - _lx) / 2) + _lx + let y = (((_ly + (_radius - 1)) - (_ly - (_radius - 1))) / 2) + + (_ly - (_radius - 1)) + Pos(x, y) + + fun radius(): I32 => _radius + fun left_pos(): Pos val => Pos(_lx, _ly) + fun right_pos(): Pos val => Pos(_rx, _ry) + fun interior_width(): I32 => (_rx - _lx) - 1 + +class OddDiamondInterior is Iterator[Pos val] + let _diamond: OddDiamondRoom box + var _y: I32 + var _x: I32 + var _radius: I32 + var _width: I32 + var _current_radius: I32 = 0 + var _y_idx: I32 = 0 + var _x_idx: I32 = 0 + + new create(diamond: OddDiamondRoom box) => + _diamond = diamond + _x = _diamond.left_pos().x + 1 + _y = _diamond.left_pos().y + _radius = _diamond.radius() + _width = _diamond.interior_width() + + fun has_next(): Bool => _x_idx < _width + + fun ref next(): Pos val => + let pos = Pos(_x, _y) + if _y_idx < (_current_radius * 2) then + _y = _y + 1 + _y_idx = _y_idx + 1 + else + _x = _x + 1 + _y_idx = 0 + _x_idx = _x_idx + 1 + if _x_idx < _radius then + _current_radius = _current_radius + 1 + else + _current_radius = _current_radius - 1 + end + _y = _diamond.left_pos().y - _current_radius + end + pos + +class EvenDiamondRoom is RoomShape + let _lx: I32 + let _ly: I32 + let _rx: I32 + let _ry: I32 + let _radius: I32 + let _perimeter_size: USize + let _pt1: I32 + let _pt2: I32 + let _pt3: I32 + + new val create(left: Pos val, right: Pos val) => + _lx = left.x + _ly = left.y + _rx = right.x + _ry = right.y + let space = (_rx - _lx) - 1 + _radius = (space.f64() / 2).ceil().i32() + _perimeter_size = ((_radius * 4) + 2).usize() + _pt1 = _radius + _pt2 = (_pt1 + _radius) + 1 + _pt3 = _pt2 + _radius + + fun perimeter_space(i': USize): Pos val ? => + let i = i'.i32() + if (i < 0) or (i >= _perimeter_size.i32()) then error end + let x = + if i <= _pt2 then + _lx + i + else + _rx - (i - _pt2) + end + let y = + if i <= _pt1 then + _ly + i + elseif i <= _pt2 then + (_ly + _radius) - ((i - _pt1) - 1) + elseif i <= _pt3 then + _ly - (i - _pt2) + else + (_ly - _radius) + ((i - _pt3) - 1) + end + Pos(x, y) + + fun interior(): Iterator[Pos val] => EvenDiamondInterior(this) + + fun rand_interior_position(): Pos val => + let x = (_rx - _lx) / 2 + let y = ((_ly + (_radius - 1)) - (_ly - (_radius - 1))) / 2 + Pos(x, y) + + fun perimeter_size(): USize => _perimeter_size + + fun radius(): I32 => _radius + fun left_pos(): Pos val => Pos(_lx, _ly) + fun right_pos(): Pos val => Pos(_rx, _ry) + fun interior_width(): I32 => (_rx - _lx) - 1 + +class EvenDiamondInterior is Iterator[Pos val] + let _diamond: EvenDiamondRoom box + var _y: I32 + var _x: I32 + var _radius: I32 + var _width: I32 + var _current_radius: I32 = 0 + var _y_idx: I32 = 0 + var _x_idx: I32 = 0 + + new create(diamond: EvenDiamondRoom box) => + _diamond = diamond + _x = _diamond.left_pos().x + 1 + _y = _diamond.left_pos().y + _radius = _diamond.radius() + _width = _diamond.interior_width() + + fun has_next(): Bool => _x_idx < _width + + fun ref next(): Pos val => + let pos = Pos(_x, _y) + if _y_idx < (_current_radius * 2) then + _y = _y + 1 + _y_idx = _y_idx + 1 + else + _x = _x + 1 + _y_idx = 0 + _x_idx = _x_idx + 1 + if _x_idx < _radius then + _current_radius = _current_radius + 1 + elseif _x_idx > _radius then + _current_radius = _current_radius - 1 + end + _y = _diamond.left_pos().y - _current_radius + end + pos + + + + + diff --git a/src/datast/scan.pony b/src/datast/scan.pony new file mode 100755 index 0000000..e1a9b39 --- /dev/null +++ b/src/datast/scan.pony @@ -0,0 +1,73 @@ +class Scan + let _tlx: I32 + let _tly: I32 + let _w: I32 + let _h: I32 + + new create(tl: Pos val, h: I32, w: I32) => + _tlx = tl.x + _tly = tl.y + _w = w + _h = h + + fun apply(): Iterator[Pos val] => ScanIterator(this, _h, _w) + fun starting_pos(): Pos val => Pos(_tlx, _tly) + +class ScanIterator is Iterator[Pos val] + let _scan: Scan box + var _y: I32 + var _x: I32 + var _h: I32 + var _w: I32 + var _y_idx: I32 = 0 + var _x_idx: I32 = 0 + + new create(scan: Scan box, h: I32, w: I32) => + _scan = scan + let start_pos = _scan.starting_pos() + _x = start_pos.x + _y = start_pos.y + _h = h + _w = w + + fun has_next(): Bool => _y_idx < _h + + fun ref next(): Pos val => + let pos = Pos(_x, _y) + if _x_idx < (_w - 1) then + _x = _x + 1 + _x_idx = _x_idx + 1 + else + _y = _y + 1 + _y_idx = _y_idx + 1 + _x = _scan.starting_pos().x + _x_idx = 0 + end + pos + +class ScanClose is Iterator[Pos val] + var _pos: Pos val + let _diffs: Array[Pos val] = [Pos(-1, -1), Pos(0, -1), Pos(1, -1), + Pos(-1, 0), Pos(1, 0), + Pos(-1, 1), Pos(0, 1), Pos(1, 1)] + var _idx: USize = 0 + + new create(pos: Pos val) => + _pos = pos + + fun ref apply(pos: Pos val): ScanClose => + _pos = pos + _idx = 0 + this + + fun has_next(): Bool => _idx < 8 + + fun ref next(): Pos val => + let pos = + try + _pos + _diffs(_idx) + else + Pos(-1, -1) + end + _idx = _idx + 1 + pos diff --git a/src/datast/test.pony b/src/datast/test.pony new file mode 100755 index 0000000..221e9aa --- /dev/null +++ b/src/datast/test.pony @@ -0,0 +1,89 @@ +use "ponytest" +use "debug" +use "collections" + +actor Main is TestList + new create(env: Env) => PonyTest(env, this) + + new make() => None + + fun tag tests(test: PonyTest) => + test(_TestRectRoomShape) + test(_TestRangedArray) + test(_TestMinHeap) + +class iso _TestRectRoomShape is UnitTest + fun name(): String => "datast:RectRoom" + + fun apply(h: TestHelper) ? => + let p1: RoomShape val = RectRoom(Pos(2, 2), Pos(4, 5)) + + let perimeter_ans = [Pos(2, 2), Pos(3, 2), Pos(4, 2), Pos(4, 3), Pos(4, 4), + Pos(4, 5), Pos(3, 5), Pos(2, 5), Pos(2, 4), Pos(2, 3)] + + for i in Range(0, 10) do + h.assert_eq[Pos val](p1.perimeter_space(i), perimeter_ans(i)) + end + +class iso _TestRangedArray is UnitTest + fun name(): String => "datast:RangedArray" + + fun apply(h: TestHelper) ? => + let r = RangedArray[String] + r.add("hi", 3) + r.add("man", 3) + r.add("cool", 1) + r.add("thing", 2) + r.add("this", 1) + r.add("is", 144) + + h.assert_eq[String](r(0), "hi") + h.assert_eq[String](r(1), "hi") + h.assert_eq[String](r(2), "hi") + h.assert_eq[String](r(3), "man") + h.assert_eq[String](r(4), "man") + h.assert_eq[String](r(5), "man") + h.assert_eq[String](r(6), "cool") + h.assert_eq[String](r(7), "thing") + h.assert_eq[String](r(8), "thing") + h.assert_eq[String](r(9), "this") + h.assert_eq[String](r(10), "is") + h.assert_eq[String](r(100), "is") + +class iso _TestMinHeap is UnitTest + fun name(): String => "datast:MinHeap" + + fun apply(h: TestHelper) ? => + let mh = MinHeap[I32] + mh.insert(5) + h.assert_eq[I32](mh.peek(), 5) + mh.insert(3) + h.assert_eq[I32](mh.peek(), 3) + mh.insert(7) + h.assert_eq[I32](mh.peek(), 3) + mh.insert(2) + h.assert_eq[I32](mh.peek(), 2) + mh.insert(10) + h.assert_eq[I32](mh.peek(), 2) + mh.insert(11) + h.assert_eq[I32](mh.peek(), 2) + mh.insert(15) + h.assert_eq[I32](mh.peek(), 2) + mh.insert(1) + h.assert_eq[I32](mh.peek(), 1) + mh.insert(12) + h.assert_eq[I32](mh.peek(), 1) + h.assert_eq[I32](mh.pop(), 1) + h.assert_eq[I32](mh.peek(), 2) + h.assert_eq[I32](mh.pop(), 2) + mh.insert(4) + h.assert_eq[I32](mh.pop(), 3) + mh.insert(20) + h.assert_eq[I32](mh.pop(), 4) + h.assert_eq[I32](mh.pop(), 5) + h.assert_eq[I32](mh.pop(), 7) + h.assert_eq[I32](mh.pop(), 10) + h.assert_eq[I32](mh.pop(), 11) + h.assert_eq[I32](mh.pop(), 12) + h.assert_eq[I32](mh.pop(), 15) + h.assert_eq[I32](mh.pop(), 20) diff --git a/src/display/acolyte-display-adapter.pony b/src/display/acolyte-display-adapter.pony new file mode 100755 index 0000000..4cea656 --- /dev/null +++ b/src/display/acolyte-display-adapter.pony @@ -0,0 +1,43 @@ +use "../datast" +use "../inventory" +use "../world" + +primitive AcolyteDisplayAdapter is DisplayAdapter[Tiles] + fun apply(t: Tiles, col: I32, row: I32): (Glyph, Color) ? => + let tile: Tile = t(Pos(col, row)) + if tile.is_visible() then + let background = + if tile.is_highlighted() then + Colors.yellow() + elseif tile.elevation >= 0 then + ElevationColors(tile.elevation) + else + TerrainColors(tile.terrain) + end + let display_char = + if tile.is_occupied() then + DisplayChars(tile.occupant_code) + elseif tile.has_item() then + try + let i = tile.item as Item val + ItemDisplayChars(i) + else + TerrainDisplayChars(tile.terrain) + end + elseif tile.has_landmark() then + LandmarkDisplayChars(tile.landmark) + else + TerrainDisplayChars(tile.terrain) + end + (display_char, background) + else + if tile.is_highlighted() then + (" ", Colors.yellow()) + elseif tile.has_staircase() and tile.has_been_seen() then + (LandmarkDisplayChars(tile.landmark), Colors.black()) + elseif tile.has_been_seen() then + (" ", Colors.black()) + else + (" ", TerrainColors(Undug)) + end + end diff --git a/src/display/colors.pony b/src/display/colors.pony new file mode 100755 index 0000000..1f0b573 --- /dev/null +++ b/src/display/colors.pony @@ -0,0 +1,62 @@ +use "../world" + +primitive Colors + fun black(): I32 => 0 + fun red(): I32 => 1 + fun green(): I32 => 2 + fun dark_yellow(): I32 => 3 + fun blue(): I32 => 4 + fun magenta(): I32 => 5 + fun cyan(): I32 => 6 + fun light_grey(): I32 => 7 + fun dark_grey(): I32 => 8 + fun pink_orange(): I32 => 9 + fun bright_green(): I32 => 10 + fun yellow(): I32 => 11 + fun turquoise(): I32 => 14 + fun white(): I32 => 15 + fun orange(): I32 => 202 + fun lava_red(): I32 => 196 + + fun light_tan(): I32 => 246 + fun mid_tan(): I32 => 242 + fun dark_tan(): I32 => 236 + fun light_green(): I32 => 46 + fun mid_green(): I32 => 76 //2 + fun dark_green(): I32 => 29 //28 + fun light_brown(): I32 => 179 //178 + fun mid_brown(): I32 => 136 + fun dark_brown(): I32 => 94 + fun ocean_blue(): I32 => 21 + +primitive ElevationColors + fun apply(code: ISize): I32 => + match code + | -1 => Colors.ocean_blue() + | 0 => Colors.light_tan() + | 1 => Colors.mid_tan() + | 2 => Colors.dark_tan() + | 3 => Colors.light_green() + | 4 => Colors.mid_green() + | 5 => Colors.dark_green() + | 6 => Colors.light_brown() + | 7 => Colors.mid_brown() + | 8 => Colors.dark_brown() + else + Colors.ocean_blue() + end + +primitive TerrainColors + fun apply(t: Terrain val): I32 => + match t + | Plain => Colors.black() + | Wall => Colors.dark_tan() + | Forest => Colors.green() + | Hill => Colors.orange() + | Floor => Colors.black() + | Lava => Colors.lava_red() + | Undug => Colors.dark_grey() + | Void => Colors.blue() + else + Colors.black() + end \ No newline at end of file diff --git a/src/display/display-adapter.pony b/src/display/display-adapter.pony new file mode 100755 index 0000000..dfc1498 --- /dev/null +++ b/src/display/display-adapter.pony @@ -0,0 +1,5 @@ +type Glyph is String +type Color is I32 + +trait DisplayAdapter[M: Any #read] + fun apply(map: M, col: I32, row: I32): (Glyph, Color) ? diff --git a/src/display/display.pony b/src/display/display.pony new file mode 100755 index 0000000..215f787 --- /dev/null +++ b/src/display/display.pony @@ -0,0 +1,350 @@ +use "collections" +use "ncurses" +use "../agents" +use "../datast" +use "../help" +use "../inventory" +use "../world" + +interface Display + be apply(tiles: Tiles iso) + be inventory(i: InventoryDisplayable val) + be stats(s: Stats val) + be log(s: String, color: I32 = 15) + be help() + be clear() + be close() + be close_with_message(msg: String) + +actor EmptyDisplay + let _env: (Env | None) + + new create(env: (Env | None) = None) => + _env = env + + be apply(tiles: Tiles iso) => None + be inventory(i: InventoryDisplayable val) => None + be stats(s: Stats val) => None + be log(s: String, color: I32 = 15) => + match _env + | let e: Env => + e.out.print(s) + end + + be help() => None + + be clear()=> None + + be close() => None + + be close_with_message(msg: String) => + @printf[I32]((msg + "\n").cstring()) + +actor CursesDisplay + let _world_height: I32 + let _world_width: I32 + let _log_width: I32 + let _log_msgs: Array[LogMsg val] = Array[LogMsg val] + let _log_max: USize + let _world_window: Pointer[Window] + let _log_window: Pointer[Window] + let _stats_window: Pointer[Window] + + new create(world_height: I32, world_width: I32, log_width: I32, + stats_height: I32) + => + _world_height = world_height + _world_width = world_width + _log_max = _world_height.usize() + _log_width = log_width + let parent = Nc.initscr() + _world_window = Nc.newwin(_world_height, _world_width, 1, 2) + _log_window = Nc.newwin(_world_height, _log_width, 0, _world_width + 10) + _stats_window = Nc.newwin(stats_height, _world_width, _world_height + 1, + 2) + Nc.clear() + Nc.wclear(_world_window) + Nc.wclear(_log_window) + Nc.wclear(_stats_window) + Nc.noecho() + Nc.cbreak() + Nc.keypad(parent, true) + Nc.curs_set(0) + _init() + for i in Range[I32](0, 256) do + Nc.switch_on_pair(i) + // Nc.printw(i.string() + " ") + Nc.switch_off_pair(i) + end + // Nc.getch() + Nc.clear() + + be apply(tiles: Tiles iso) => + let t: Tiles ref = consume tiles + for row in Range[I32](0, _world_height) do + for col in Range[I32](0, _world_width) do + try + let tile: Tile = t(Pos(col, row)) + if tile.is_visible() then + let background = + if tile.is_highlighted() then + Colors.yellow() + elseif tile.elevation >= 0 then + ElevationColors(tile.elevation) + else + TerrainColors(tile.terrain) + end + let display_char = + if tile.is_occupied() then + DisplayChars(tile.occupant_code) + elseif tile.has_item() then + try + let i = tile.item as Item val + ItemDisplayChars(i) + else + TerrainDisplayChars(tile.terrain) + end + elseif tile.has_landmark() then + LandmarkDisplayChars(tile.landmark) + else + TerrainDisplayChars(tile.terrain) + end + _display_tile(row, col, display_char, background) + else + if tile.is_highlighted() then + _display_tile(row, col, " ", Colors.yellow()) + elseif tile.has_staircase() and tile.has_been_seen() then + _display_tile(row, col, LandmarkDisplayChars(tile.landmark), + Colors.black()) + elseif tile.has_been_seen() then + _display_tile(row, col, " ", Colors.black()) + else + _display_tile(row, col, " ", TerrainColors(Undug)) + end + end + end + end + end + Nc.refresh() + Nc.wrefresh(_world_window) + + be inventory(inv: InventoryDisplayable val) => + Nc.wclear(_world_window) + let categories = ["Weapons", "Armor", "Potions", "Miscellaneous"] + let mid_offset = _world_width / 2 + var line: I32 = 0 + var offset: I32 = 0 + for category in categories.values() do + try + let items = inv.items(category) + if items.size() > 0 then + Nc.mvwprintw(_world_window, line, offset, category.upper()) + line = line + 1 + if (line > 27) and (offset == 0) then + line = 0 + offset = mid_offset + end + end + for (idx, v) in items.pairs() do + let item = if inv.equipped.contains(category) + and inv.equipped(category).contains(idx) then + " * " + v + else + " " + v + end + let highlighted = (inv.highlighted._1 == category) + and (idx == inv.highlighted._2) + _display_item(line, item, highlighted, offset) + line = line + 1 + if (line > 26) and (offset == 0) then + line = 0 + offset = mid_offset + end + end + end + end + Nc.refresh() + Nc.wrefresh(_world_window) + + be help() => + Nc.wclear(_world_window) + var line: I32 = 0 + for item in Help().values() do + _display_item(line, item, false, 0) + line = line + 1 + end + Nc.refresh() + Nc.wrefresh(_world_window) + + fun ref _display_item(line: I32, item: String, highlighted: Bool, + offset: I32) => + if highlighted then + Nc.wswitch_on_pair(_world_window, Colors.light_grey()) + Nc.mvwprintw(_world_window, line, offset, item) + Nc.wswitch_off_pair(_world_window, Colors.light_grey()) + else + Nc.mvwprintw(_world_window, line, offset, item) + end + + be stats(s: Stats val) => + Nc.wclear(_stats_window) + Nc.mvwprintw(_stats_window, 0, 0, s.string()) + Nc.refresh() + Nc.wrefresh(_stats_window) + + fun ref _display_tile(row: I32, col: I32, display_char: String, + background: I32) => + Nc.wswitch_on_pair(_world_window, background) + Nc.mvwaddch(_world_window, row, col, display_char) + Nc.wswitch_off_pair(_world_window, background) + + be log(s: String, color: I32 = 15) => + for msg in LogAppender(s, _log_width, color).values() do + _log_msgs.push(msg) + end + + while _log_msgs.size() > _log_max do + try _log_msgs.shift() end + end + _display_logs() + + be clear() => + Nc.wclear(_world_window) + Nc.wclear(_log_window) + Nc.wclear(_stats_window) + + fun _check_for_dash(s: String, orig_len: ISize, used: ISize): String => + if used < orig_len then + s + "-" + else + s + end + + fun ref _display_logs() => + Nc.wclear(_log_window) + for i in Range[I32](1, _log_msgs.size().i32()) do + try + Nc.mvwprintw(_log_window, i, 0, _log_msgs(i.usize()).msg) + end + end + Nc.refresh() + Nc.wrefresh(_log_window) + + fun _init() => + Nc.start_color() + for i in Range[I32](0, 256) do + Nc.init_pair(i, Colors.white(), i) + end + + be close() => + _close() + + be close_with_message(msg: String) => + _close() + @printf[I32]((msg + "\n").cstring()) + + fun ref _close() => + // Clear ncurses data structures + Nc.delwin(_world_window) + Nc.delwin(_log_window) + Nc.endwin() + + +primitive LogAppender + fun apply(s: String, line_length: I32, color: I32 = 15): + Array[LogMsg val] val + => + let log_msgs: Array[LogMsg val] trn = + recover Array[LogMsg val] end + try + let str_len = s.size().i32() + if str_len <= line_length then + log_msgs.push(LogMsg(s, color)) + else + let words = recover val s.split(" ") end + if words.size() == 0 then return consume log_msgs end + + var next_line = words(0) + + for i in Range(1, words.size()) do + let word = words(i) + if word == " " then continue end + if (next_line.size() + word.size() + 1) <= line_length.usize() then + next_line = next_line + " " + word + else + try + if next_line(0) == ' ' then + next_line = next_line.substring(1, next_line.size().isize()) + end + end + log_msgs.push(LogMsg(next_line, color)) + next_line = word + end + end + log_msgs.push(LogMsg(next_line, color)) + end + consume log_msgs + else + recover [LogMsg("Error printing message!", color)] end + end + +primitive DisplayChars + fun apply(code: I32): String => + match code + | OccupantCodes.self() => "@" + | OccupantCodes.raven() => "r" + | OccupantCodes.goblin() => "g" + | OccupantCodes.brigand() => "b" + | OccupantCodes.ooze() => "o" + | OccupantCodes.skeleton() => "s" + | OccupantCodes.ekek() => "e" + | OccupantCodes.hellhound() => "h" + | OccupantCodes.cloaked_shadow() => "c" + | OccupantCodes.mantis() => "m" + | OccupantCodes.horror() => "H" + | OccupantCodes.vampire() => "v" + else + " " + end + +primitive TerrainDisplayChars + fun apply(t: Terrain val): String => + match t + | Hill => "^" + | Floor => "." + | Wall => "#" + | Door => "|" + | Lava => "^" + | Undug => " " + | Void => " " + else + " " + end + +primitive LandmarkDisplayChars + fun apply(l: Landmark): String => + match l + | UpStairs => "<" + | let d: DownStairs val => ">" + else + " " + end + +primitive ItemDisplayChars + fun apply(i: Item val): String => + match i + | let a: Armor val => "%" + | let a: Weapon val => "%" + | let a: Potion val => "!" + | let a: Gold val => "$" + else + "?" + end + +class LogMsg + let msg: String + let color: I32 + + new val create(m: String, c: I32) => + msg = m + color = c diff --git a/src/display/ncurses/ncurses.pony b/src/display/ncurses/ncurses.pony new file mode 100755 index 0000000..53012bb --- /dev/null +++ b/src/display/ncurses/ncurses.pony @@ -0,0 +1,60 @@ +use "lib:ncurses" + +primitive Window + +primitive Nc + // Windows + fun initscr(): Pointer[Window] => @initscr[Pointer[Window]]() + fun newwin(nlines: I32, ncolumns: I32, begin_y: I32, begin_x: I32) + : Pointer[Window] => + @newwin[Pointer[Window]](nlines, ncolumns, begin_y, begin_x) + fun delwin(w: Pointer[Window]) => @delwin[None](w) + fun endwin() => @endwin[None]() + + // Initialization + fun noecho() => @noecho[None]() + fun cbreak() => @cbreak[None]() + fun keypad(w: Pointer[Window], bf: Bool) => + @keypad[None](w, bf) + fun curs_set(option: I32) => @curs_set[None](option) + + // State changes + fun clear() => @clear[None]() + fun wclear(window: Pointer[Window]) => @wclear[None](window) + fun refresh() => @refresh[None]() + fun wrefresh(window: Pointer[Window]) => @wrefresh[None](window) + fun erase(row: I32, column: I32) => @erase[None](row, column) + + // Output + fun printw(s: String) => @printw[None](s.cstring()) + fun wprintw(window: Pointer[Window], s: String) => @wprintw[None](window, s.cstring()) + fun mvprintw(row: I32, column: I32, s: String) => + @mvprintw[None](row, column, s.cstring()) + fun mvwprintw(window: Pointer[Window], row: I32, column: I32, s: String) => + @mvwprintw[None](window, row, column, s.cstring()) + fun mvaddch(row: I32, column: I32, char_string: String) => + try + let char = char_string.array()(0) + @mvaddch[None](row, column, char) + end + fun mvwaddch(window: Pointer[Window], row: I32, column: I32, + char_string: String) => + try + let char = char_string.array()(0) + @mvwaddch[None](window, row, column, char) + end + + // Input + fun getch(): I32 => @getch[I32]() + + // Color + fun start_color() => @start_color[None]() + fun init_pair(pair_id: I32, foreground: I32, background: I32) => + @init_pair[None](pair_id, foreground, background) + + fun switch_on_pair(id: I32) => @attron[None](id << 8) + fun wswitch_on_pair(window: Pointer[Window], id: I32) => + @wattron[None](window, id << 8) + fun switch_off_pair(id: I32) => @attroff[None](id << 8) + fun wswitch_off_pair(window: Pointer[Window], id: I32) => + @wattroff[None](window, id << 8) diff --git a/src/encounters/per-room-agent-placements.pony b/src/encounters/per-room-agent-placements.pony new file mode 100755 index 0000000..4a12abc --- /dev/null +++ b/src/encounters/per-room-agent-placements.pony @@ -0,0 +1,118 @@ +use "collections" +use "../agents" +use "../datast" +use "../display" +use "../game" +use "../rand" +use "../world" + +primitive PerRoomAgentPlacements + fun apply(tiles: Tiles, world: World tag, turn_manager: TurnManager tag, + depth: I32, self: (Self | None) = None, r: Rand iso = recover Rand end) + => + let agents: Array[Agent tag] = Array[Agent tag] + let agent_depths = AgentDepths(depth) + let rand: Rand = consume r + let room_count = tiles.room_count() + let agent_count: USize = room_count / 2 + try + let s = self as Self + s.update_next_level_xp(agent_count.i32() * 20) + end + for i in Range(0, agent_count) do + let room_idx = Rand.usize_between(0, room_count - 1) + try + let room = tiles.room(room_idx) + let pos = room.rand_interior_position() + let roll = rand.usize_between(0, agent_depths.size() - 1) + try + agent_depths(roll)(pos, world, turn_manager) + end + end + end + +primitive AgentDepths + fun apply(depth: I32): + RangedArray[{(Pos val, World tag, TurnManager tag)} val] + => + match depth + | 1 => + let r = RangedArray[{(Pos val, World tag, TurnManager tag)} val] + let f1 = {(p: Pos val, w: World tag, t: TurnManager tag) => + Raven(p, w, t)} + let f2 = {(p: Pos val, w: World tag, t: TurnManager tag) => + Goblin(p, w, t)} + r.add(f1, 1) + r.add(f2, 1) + r + | 2 => + AgentDepths(1) + | 3 => + let r = AgentDepths(2) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Brigand(p, w, t)} + r.add(f, 2) + r + | 4 => + let r = AgentDepths(3) + // let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + // Ooze(p, w, t)} + // r.add(f, 3) + r + | 5 => + let r = AgentDepths(3) + let f1 = {(p: Pos val, w: World tag, t: TurnManager tag) => + Brigand(p, w, t)} + // let f2 = {(p: Pos val, w: World tag, t: TurnManager tag) => + // Ooze(p, w, t)} + let f3 = {(p: Pos val, w: World tag, t: TurnManager tag) => + Skeleton(p, w, t)} + r.add(f1, 3) + // r.add(f2, 1) + r.add(f3, 6) + r + | 6 => + let r = AgentDepths(5) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Ekek(p, w, t)} + r.add(f, 6) + r + | 7 => + let r = AgentDepths(6) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Hellhound(p, w, t)} + r.add(f, 12) + r + | 8 => + let r = AgentDepths(7) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + CloakedShadow(p, w, t)} + r.add(f, 12) + r + | 9 => + let r = AgentDepths(8) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Mantis(p, w, t)} + r.add(f, 24) + r + | 10 => + let r = AgentDepths(9) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Horror(p, w, t)} + r.add(f, 24) + r + | 11 => + let r = AgentDepths(10) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Vampire(p, w, t)} + r.add(f, 48) + r + | 12 => + let r = AgentDepths(11) + let f = {(p: Pos val, w: World tag, t: TurnManager tag) => + Vampire(p, w, t)} + r.add(f, 48) + r + else + RangedArray[{(Pos val, World tag, TurnManager tag)} val] + end diff --git a/src/encounters/per-room-initial-placements.pony b/src/encounters/per-room-initial-placements.pony new file mode 100755 index 0000000..0fec141 --- /dev/null +++ b/src/encounters/per-room-initial-placements.pony @@ -0,0 +1,242 @@ +use "../agents" +use "../game" +use "../datast" +use "../world" +use "../rand" +use "../inventory" +use "collections" +use "../display" + +primitive PerRoomInitialPlacements + fun apply(ts: Tiles iso, depth: I32, d: Display tag, + max_depth: I32 = 12, r: Rand iso = recover Rand end): Tiles iso^ + => + recover + let tiles: Tiles = consume ts + let rand: Rand = consume r + let room_count = tiles.room_count() + let item_count = room_count / 3 + for i in Range(0, item_count) do + let room_idx = Rand.usize_between(0, room_count - 1) + try + let room = tiles.room(room_idx) + let pos = room.rand_interior_position() + let item: Item val = generate_item(depth, rand) + tiles(pos).set_item(item) + end + end + if depth == max_depth then + let room_idx = Rand.usize_between(0, room_count - 1) + try + let room = tiles.room(room_idx) + let pos = room.rand_interior_position() + tiles(pos).set_item(StaffOfEternity) + end + end + tiles + end + + fun generate_item(depth: I32, rand: Rand): Item val => + let roll = rand.i32_between(1, 5) + try + match roll + | 1 => + let heal = rand.i32_between(1, (depth + 1) * 3) + PotionBuilder(heal) + | 2 => _choose_misc_item(rand) + | 3 => GoldBuilder(rand.i32_between(1, 20)) + | 4 => + let weapon_range = WeaponDepths(depth) + let max_dmg = WeaponDepths.max_damage(depth) + let potential_bonus = WeaponDepths.potential_bonus(depth) + let bonus: I32 = rand.i32_between(0, potential_bonus) + let choice = rand.usize_between(0, weapon_range.size() - 1) + var weapon = (weapon_range(choice)()) as Weapon val + if (weapon.dmg() + bonus) > max_dmg then + weapon + else + weapon.enhance(bonus) + end + | 5 => + let armor_range = ArmorDepths(depth) + let max_ac = ArmorDepths.max_ac(depth) + let potential_bonus = ArmorDepths.potential_bonus(depth) + let bonus: I32 = rand.i32_between(0, potential_bonus) + let choice = rand.usize_between(0, armor_range.size() - 1) + var armor = (armor_range(choice)()) as Armor val + if (armor.ac() + bonus) > max_ac then + armor + else + armor.enhance(bonus) + end + else + _choose_misc_item(rand) + end + else + JarBuilder() + end + + fun _choose_misc_item(rand: Rand): Item val => + match rand.i32_between(1, 7) + | 1 => JarBuilder() + | 2 => BottleBuilder() + | 3 => BookBuilder() + | 4 => StringBuilder() + | 5 => CardsBuilder() + | 6 => SoapBuilder() + | 7 => BrassBellBuilder() + else + JarBuilder() + end + +primitive WeaponDepths + fun apply(depth: I32): RangedArray[ItemBuilder val] => + if depth == 1 then + WeaponLevels(1) + elseif depth == 2 then + WeaponLevels(2) + elseif depth < 5 then + WeaponLevels(3) + elseif depth < 7 then + WeaponLevels(4) + else + WeaponLevels(5) + end + + fun max_damage(depth: I32): I32 => + depth + 5 + + fun potential_bonus(depth: I32): I32 => + if depth > 8 then + 3 + elseif depth > 5 then + 2 + elseif depth > 2 then + 1 + else + 0 + end + +primitive WeaponLevels + fun apply(level: I32): RangedArray[ItemBuilder val] => + match level + | 1 => + // 1d4 + let r = RangedArray[ItemBuilder val] + r.add(ClubBuilder, 1) + r.add(DaggerBuilder, 1) + r.add(SilverDaggerBuilder, 1) + r + | 2 => + // 1d6 + let r = WeaponLevels(1) + r.add(QuarterstaffBuilder, 2) + r.add(HandAxeBuilder, 2) + r.add(ShortSwordBuilder, 2) + r.add(LightHammerBuilder, 2) + r + | 3 => + // 1d8 + let r = WeaponLevels(2) + r.add(MaceBuilder, 4) + r.add(BattleAxeBuilder, 4) + r.add(LongSwordBuilder, 4) + r.add(WarhammerBuilder, 4) + r + | 4 => + // 1d10 + let r = WeaponLevels(3) + r.add(GreatAxeBuilder, 16) + r.add(BroadSwordBuilder, 16) + r + | 5 => + let r = WeaponLevels(4) + r.add(MaceBuilder, 4) + r.add(BattleAxeBuilder, 4) + r.add(LongSwordBuilder, 4) + r.add(WarhammerBuilder, 4) + r + else + RangedArray[ItemBuilder val] + end + +primitive ArmorDepths + fun apply(depth: I32): RangedArray[ItemBuilder val] => + if depth == 1 then + ArmorLevels(2) + elseif depth < 3 then + ArmorLevels(3) + elseif depth < 5 then + ArmorLevels(4) + elseif depth < 6 then + ArmorLevels(5) + else + ArmorLevels(6) + end + + fun max_ac(depth: I32): I32 => + match depth + | 1 => 3 + | 2 => 4 + | 3 => 4 + | 4 => 5 + | 5 => 5 + | 6 => 6 + | 7 => 7 + | 8 => 7 + | 9 => 8 + | 10 => 8 + | 11 => 9 + | 12 => 9 + else + 10 + end + + fun potential_bonus(depth: I32): I32 => + if depth > 8 then + 3 + elseif depth > 5 then + 2 + elseif depth > 2 then + 1 + else + 0 + end + +primitive ArmorLevels + fun apply(level: I32): RangedArray[ItemBuilder val] => + match level + | 1 => + let r = RangedArray[ItemBuilder val] + r.add(LeatherArmorBuilder, 1) + r + | 2 => + let r = ArmorLevels(1) + r.add(RingMailBuilder, 1) + r.add(BucklerBuilder, 1) + r + | 3 => + let r = ArmorLevels(2) + r.add(ScaleMailBuilder, 3) + r.add(BucklerBuilder, 1) + r + | 4 => + let r = ArmorLevels(3) + r.add(ChainMailBuilder, 6) + r.add(BucklerBuilder, 1) + r + | 5 => + let r = ArmorLevels(4) + r.add(GreatShieldBuilder, 2) + r + | 6 => + let r = ArmorLevels(5) + r.add(PlateMailBuilder, 8) + r.add(GreatShieldBuilder, 2) + r + else + RangedArray[ItemBuilder val] + end + + + diff --git a/src/game/game.pony b/src/game/game.pony new file mode 100755 index 0000000..e7fa6c1 --- /dev/null +++ b/src/game/game.pony @@ -0,0 +1,321 @@ +use "../world" +use "../display" +use "../agents" +use "../input" +use "../datast" +use "collections" +use "term" + +type GameMode is ( + SplashMode | + LevelMode | + InventoryMode | + HelpMode | + DroppingMode | + PrepareFastMode | + FastMode | + LookMode | + MapViewMode | + VictoryMode | + QuittingMode +) + +// Splash screen +primitive SplashMode +// Standard game playing mode +primitive LevelMode +// Looking at inventory +primitive InventoryMode +// Help +primitive HelpMode +// Checking if you really want to drop an item +primitive DroppingMode +// Ready to receive direction for FastMode +primitive PrepareFastMode +// Automatically moving in one direction as far as you can +primitive FastMode +// Looking around map and inspecting with cursor +primitive LookMode +// Looking around map by jumping screens +primitive MapViewMode +primitive VictoryMode +primitive QuittingMode + +actor Game + var _mode: GameMode = LevelMode + var _running: Bool = true + let _env: Env + let _seed: U64 + let _display_height: I32 = 27 + let _display_width: I32 = 61 + let _log_width: I32 = 27 + let _stats_height: I32 = 5 + let _starting_pos: Pos val + let _self: Self + let _looker: Looker tag + let _map_viewer: MapViewer tag + let _display: Display tag + let _term: ANSITerm + let _turn_manager: TurnManager + var _world: World tag + var _focus: Pos val + var _saved_focus: Pos val + var _looping: Bool = false + var _loop_counter: USize = 0 + var _mid_turn: Bool = false + + //TODO: Once fast bug is fixed, fast should always be enabled and this + //can be removed. + let _enable_fast: Bool + + new create(env: Env, seed: U64, is_overworld: Bool = false, + noscreen: Bool = false, see_input: Bool = false, + is_simple_dungeon: Bool = false, enable_fast: Bool = false) + => + let world_diameter: I32 = 50//if is_overworld then 3250 else 50 end + _env = env + _seed = seed + _starting_pos = Pos((world_diameter / 2), (world_diameter / 2)) + _focus = _starting_pos + _saved_focus = _focus + _display = if noscreen then + EmptyDisplay(env) + else + CursesDisplay(_display_height, _display_width, _log_width, + _stats_height) + end + _turn_manager = TurnManager(this, _display) + + _self = Self(_turn_manager, _display, this) + _world = + if is_overworld then + OverWorld(world_diameter, _turn_manager, _seed, _display) + elseif is_simple_dungeon then + SimpleDungeon(10, _turn_manager, _display where self = _self) + else + Dungeon(world_diameter, _turn_manager, _display where self = _self) + end + _looker = Looker(_world, _focus, _display_height, _display_width, _display) + _map_viewer = MapViewer(_world, _focus, _display_height, _display_width, + _display) + + // Setup input + let term = ANSITerm(InputNotify(this), env.input) + let notify = object iso + let term: ANSITerm = term + fun ref apply(data: Array[U8] iso) => term(consume data) + fun ref dispose() => term.dispose() + end + _term = term + env.input(consume notify) + + _enable_fast = enable_fast + + // Called after InputNotify checks that Terminal is a valid size + be start() => + _display.log("") + _log_initial_msgs() + _world.enter(_self) + display_world() + display_stats() + + be apply(cmd: Cmd val) => + match cmd + | QuitCmd => + _display.log("You sure you want to quit? y/n") + _mode = QuittingMode + | ResetCmd => + _display.log("Resetting turn and command queues.") + stop_loop() + exit_fast_mode() + _self.clear_commands() + _turn_manager.panic() + // TODO: Always enable fast when bug with getting stuck at wall is fixed. + | FastCmd => + if _enable_fast then + if _mode is LevelMode then + _display.log("Repeat which command?") + _mode = PrepareFastMode + elseif _mode is PrepareFastMode then + _display.log("Cancelled") + _mode = LevelMode + end + else + _display.log("Unrecognized Cmd") + end + | EmptyCmd => + None + else + match (_mode, cmd) + // LevelMode + | (LevelMode, HelpCmd) => + _display.log("Displaying help...") + _display.help() + _mode = HelpMode + | (LevelMode, InventoryModeCmd) => + _mode = InventoryMode + _display.log("-------------------------") + _display.log("INVENTORY COMMANDS") + _display.log("-------------------------") + _display.log(" - select item") + _display.log(" - equip/use/drink") + _display.log("d - (d)rop item") + _display.log("l - (l)ook at item") + _display.log("i/ - exit inventory") + _display.log("-------------------------") + _self.process_inventory_command(cmd, _display) + | (LevelMode, LookCmd) => + _display.log("Look where?") + _mode = LookMode + _looker.init(_display_height, _display_width, _focus, _world) + | (LevelMode, ViewCmd) => + _display.log("Move cursor to see map") + _mode = MapViewMode + _map_viewer.init(_focus, _world) + | (LevelMode, _) => + if _running and not _mid_turn then + _self.enqueue_command(cmd) + _turn_manager.next_turn(_world, _focus) + _mid_turn = true + end + // LookMode // + | (LookMode, EscCmd) => + _mode = LevelMode + _looker.close(_display_height, _display_width, _focus) + | (LookMode, LookCmd) => + _mode = LevelMode + _looker.close(_display_height, _display_width, _focus) + | (LookMode, _) => + _looker(cmd) + // MapViewMode // + | (MapViewMode, EscCmd) => + _mode = LevelMode + _map_viewer.close(_focus) + | (MapViewMode, ViewCmd) => + _mode = LevelMode + _map_viewer.close(_focus) + | (MapViewMode, _) => + _map_viewer(cmd) + // InventoryMode // + | (InventoryMode, EscCmd) => + _mode = LevelMode + _display.log("-------------------------") + _self.exit_inventory_mode() + display_world() + | (InventoryMode, DropCmd) => + _display.log("You sure you want to drop? y/n") + _mode = DroppingMode + | (InventoryMode, InventoryModeCmd) => + _mode = LevelMode + _display.log("-------------------------") + _self.exit_inventory_mode() + display_world() + | (InventoryMode, _) => + _self.process_inventory_command(cmd, _display) + // HelpMode + | (HelpMode, _) => + _mode = LevelMode + _display.log("Exiting help...") + display_world() + // DroppingMode // + | (DroppingMode, YCmd) => + _mode = InventoryMode + _self.process_inventory_command(DropCmd, _display) + | (DroppingMode, _) => + _display.log("That was close!") + _mode = InventoryMode + // PrepareFastMode // + | (PrepareFastMode, EscCmd) => + _display.log("Cancelled") + _mode = LevelMode + | (PrepareFastMode, _) => + _self.enter_fast_mode(cmd) + _mode = FastMode + _looping = true + loop() + // FastMode // + | (FastMode, _) => + _exit_fast_mode() + _mode = LevelMode + // QuittingMode // + | (QuittingMode, YCmd) => + _term.dispose() + _end_game() + | (QuittingMode, _) => + _display.log("Good choice!") + _mode = LevelMode + end + end + + be loop() => + if _looping and _running then + _turn_manager.loop_next_turn(_world, _focus) + end + + be stop_loop() => _stop_loop() + + fun ref _stop_loop() => + _turn_manager.stop_loop(_self) + _looping = false + + be exit_fast_mode() => + _exit_fast_mode() + + fun ref _exit_fast_mode() => + _stop_loop() + _self.exit_fast_mode() + _mode = LevelMode + + be next_turn() => + display_world() + _mid_turn = false + + be update_world(w: World tag) => + _world = w + update_seen() + display_world() + + be update_focus(pos: Pos val) => _focus = pos + + be update_seen() => _world.update_seen(_display_height, _display_width, + _focus) + + be display_world() => + _world.display_map(_display_height, _display_width, _focus, _display) + + be display_stats() => _self.display_stats() + + be log(s: String) => _display.log(s) + + be increment_turn() => _self.increment_turn() + + be fail_term_too_small() => + _display.close_with_message("\nTerminal must be at least 99x31! Please resize and start Acolyte again.\n") + _term.dispose() + + be win() => + _display.log("You have won the game!") + _mode = VictoryMode + + fun ref _end_game() => + _display.close() + _term.dispose() + _running = false + + fun _log_initial_msgs() => + _display.log("=-----------=") + _display.log("|-=-=-=-=-=-|") + _display.log("||--=-=-=--||") + _display.log("|||acolyte|||") + _display.log("||--=-=-=--||") + _display.log("|-=-=-=-=-=-|") + _display.log("=-----------=") + _display.log("") + _display.log("**************************") + _display.log("*Resizing terminal window*") + _display.log("*smaller than 99x31 will *") + _display.log("*shut down game *") + _display.log("**************************") + _display.log("") + _display.log("Press h for command list.") + _display.log("") diff --git a/src/game/turn-manager.pony b/src/game/turn-manager.pony new file mode 100755 index 0000000..c443093 --- /dev/null +++ b/src/game/turn-manager.pony @@ -0,0 +1,126 @@ +use "collections" +use "time" +use "../agents" +use "../datast" +use "../display" +use "../world" + +actor TurnManager + var pending_turn: (World tag | None) = None + var last_rank_acted: I32 = -1 + var current_expected_acks: USize = 0 + var acks_left: USize = 0 + var ready_map: Map[USize, Agent tag] = Map[USize, Agent tag] + let game: Game + let display: Display tag + var looping: Bool = false + var stopped: Bool = false + // Current position of Self + var self_pos: Pos val = Pos(-1, -1) + + new create(g: Game, d: Display tag) => + game = g + display = d + + be clear() => + acks_left = 0 + looping = false + + be next_turn(w: World tag, sp: Pos val) => + self_pos = sp + pending_turn = w + _start_next() + + be loop_next_turn(w: World tag, sp: Pos val) => + looping = true + next_turn(w, self_pos) + + be stop_loop(self: Self tag) => + looping = false + _end_turn() + + fun ref _start_next() => + try + (pending_turn as World tag).next_turn(this, self_pos) + game.increment_turn() + end + + be set_expected_acks(expected: USize) => + current_expected_acks = expected + acks_left = expected + + be ack_ready(rank: USize, agent: Agent tag) => + ready_map(rank) = agent + if (rank.i32() - last_rank_acted) == 1 then + last_rank_acted = rank.i32() + agent.act() + try + while ready_map.contains(last_rank_acted.usize() + 1) do + ready_map(last_rank_acted.usize() + 1).act() + last_rank_acted = last_rank_acted + 1 + end + end + end + + be ack() => + if not stopped then + if acks_left > 0 then + acks_left = acks_left - 1 + if (acks_left == 0) then + game.display_stats() + if looping then + game.loop() + end + _end_turn() + end + else + // TODO: This shouldn't happen + // game.log("!!--!!") + // game.log("--!!--") + // game.log("!!--!!") + None + end + end + + fun ref _end_turn() => + game.next_turn() + ready_map.clear() + last_rank_acted = -1 + game.update_seen() + + be report_death(a: Agent tag, pos: Pos val, w: World tag) => + acks_left = acks_left + 1 + w.process_death(a, pos) + + be update_focus(pos: Pos val) => + game.update_focus(pos) + + be panic() => + // Just in case anything were to go wrong, this can clear, pause, + // and reset + if looping then + game.stop_loop() + end + try (pending_turn as World tag).stop_agents() end + stopped = true + let timers = Timers + let timer = Timer(PanicNotify(this), 500_000_000) + timers(consume timer) + + be recover_from_panic() => + acks_left = 0 + try (pending_turn as World tag).restart_agents() end + game.display_stats() + _end_turn() + stopped = false + +class PanicNotify is TimerNotify + let _turn_manager: TurnManager + + new iso create(turn_manager: TurnManager) => + _turn_manager = turn_manager + + fun ref apply(timer: Timer, count: U64): Bool => + _turn_manager.recover_from_panic() + false + diff --git a/src/generators/diamond-square.pony b/src/generators/diamond-square.pony new file mode 100755 index 0000000..c2cf686 --- /dev/null +++ b/src/generators/diamond-square.pony @@ -0,0 +1,136 @@ +use "random" +use "time" +use "../datast" +use "../world" + +primitive DiamondSquare + fun apply(diameter: I32, diameter_per_region: I32, rand: Random): + Matrix[F64] ? + => + _DiamondSquare(diameter, diameter_per_region)(rand) + +class _DiamondSquare + let _diameter_per_region: I32 + let _diameter: I32 + + new create(diameter: I32, diameter_per_region: I32) => + _diameter_per_region = diameter_per_region + _diameter = diameter + + fun apply(rand: Random): Matrix[F64] iso^ ? => + var matrix: Matrix[F64] iso = create_corner_values(rand) + let d_per_region = _diameter_per_region + + //Run diamondSquare on each subsection and add it as a matrix to newMatrixOfMatrices + var y: I32 = 0 + var x: I32 = 0 + while y < _diameter do + while x < _diameter do + matrix = diamond_square(consume matrix, x, y, d_per_region, rand) + x = x + _diameter_per_region + end + x = 0 + y = y + _diameter_per_region + end + consume matrix + + fun create_corner_values(rand: Random): Matrix[F64] iso^ ? => + let d = _diameter + let matrix: Matrix[F64] iso = recover Matrix[F64](d, d) end + + //Top + var x: I32 = 0 + while x < _diameter do + let point = _random_elevation(rand) + matrix(Pos(x, 0)) = point + _small_noise(rand) + if (x - 1) > 0 then + matrix(Pos(x - 1, 0)) = point + _small_noise(rand) + end + x = x + _diameter_per_region + end + + //Right + var y: I32 = 0 + while y < _diameter do + let point = _random_elevation(rand) + matrix(Pos(_diameter - 1, y)) = point + _small_noise(rand) + if (y - 1) > 0 then + matrix(Pos(_diameter - 1, y - 1)) = point + _small_noise(rand) + end + y = y + _diameter_per_region + end + + matrix(Pos(d - 1, d - 1)) = _random_elevation(rand) + + //Bottom + x = 0 + while x < _diameter do + let point = _random_elevation(rand) + matrix(Pos(x, _diameter - 1)) = point + _small_noise(rand) + if (x - 1) > 0 then + matrix(Pos(x - 1, _diameter - 1)) = point + _small_noise(rand) + end + x = x + _diameter_per_region + end + + //Remaining + x = 0 + y = 0 + while y < _diameter do + while x < _diameter do + let point = _random_elevation(rand) + matrix(Pos(x, y)) = point + _small_noise(rand) + if (y - 1) > 0 then + matrix(Pos(x, y - 1)) = point + _small_noise(rand) + end + if (x - 1) > 0 then + matrix(Pos(x - 1, y)) = point + _small_noise(rand) + end + if ((x - 1) > 0) and ((y - 1) > 0) then + matrix(Pos(x - 1, y - 1)) = point + _small_noise(rand) + end + x = x + _diameter_per_region + end + x = 0 + y = y + _diameter_per_region + end + + consume matrix + + fun diamond_square(m: Matrix[F64] iso, x: I32, y: I32, + diameter: I32, rand: Random): Matrix[F64] iso^ + => + var matrix: Matrix[F64] iso = consume m + let midpoint: I32 = _find_midpoint(diameter) + try + let nw: F64 = matrix(Pos(x, y)) + let ne: F64 = matrix(Pos(x + (diameter - 1), y)) + let sw: F64 = matrix(Pos(x, y + (diameter - 1))) + let se: F64 = matrix(Pos(x + (diameter - 1), y + (diameter - 1))) + matrix(Pos(x + midpoint, y + midpoint)) = ((nw + ne + sw + se) / 4) + _small_noise(rand) + matrix(Pos(x, y + midpoint)) = ((nw + sw) / 2) + _small_noise(rand) + matrix(Pos(x + (diameter - 1), y + midpoint)) = ((ne + se) / 2) + _small_noise(rand) + matrix(Pos(x + midpoint, y)) = ((nw + ne) / 2) + _small_noise(rand) + matrix(Pos(x + midpoint, y + (diameter - 1))) = ((sw + se) / 2) + _small_noise(rand) + else + @printf[None](("Failed!\n").cstring()) + end + if midpoint == 1 then return consume matrix end + matrix = diamond_square(consume matrix, x, y, midpoint + 1, rand) + matrix = diamond_square(consume matrix, x, y + midpoint, midpoint + 1, + rand) + matrix = diamond_square(consume matrix, x + midpoint, y, midpoint + 1, + rand) + matrix = diamond_square(consume matrix, x + midpoint, y + midpoint, + midpoint + 1, rand) + + consume matrix + + fun _find_midpoint(diameter: I32): I32 => + diameter / 2 + + fun _random_elevation(rand: Random): F64 => + rand.int(9).f64() + + fun _small_noise(rand: Random): F64 => + (rand.real() - 0.5) / 2 diff --git a/src/generators/digout.pony b/src/generators/digout.pony new file mode 100755 index 0000000..b0874fd --- /dev/null +++ b/src/generators/digout.pony @@ -0,0 +1,180 @@ +use "random" +use "time" +use "../datast" +use "../world" +use "../rand" +use "../guid" +use "../inventory" +use "collections" + +primitive Digout + fun apply(diameter: I32, starting_pos: Pos val, depth: I32, + rand: Rand iso = recover Rand end): Tiles iso^ + => + let d: _Digout val = _Digout(diameter, starting_pos, depth) + recover d(consume rand) end + +class _Digout + let diameter: I32 + let starting_pos: Pos val + let depth: I32 + let max_depth: I32 + + new val create(d: I32, spos: Pos val, dep: I32, max: I32 = 12) => + diameter = d + starting_pos = spos + depth = dep + max_depth = max + + fun val apply(rand: Rand iso): Tiles iso^ => + // TODO: Use determinate seed + let guid_gen: GuidGenerator = GuidGenerator() + let d = diameter + var tiles: Tiles iso = recover Tiles(d, d) end + + let w = rand.i32_between(3, 7) + let h = rand.i32_between(3, 7) + + let shape = RectRoom(starting_pos - Pos(w, h), starting_pos + Pos(w, h)) + let guid = guid_gen() + tiles.add_room(guid, shape) + for pos in shape.perimeter() do + try tiles(pos) = + recover + Tile(EmptyOccupant, OccupantCodes.none(), Wall, -1 + where r_id = guid) + end + end + end + + for pos in shape.interior() do + try tiles(pos) = + recover + Tile(EmptyOccupant, OccupantCodes.none(), Floor, -1 + where r_id = guid) + end + end + end + + for i in Range(0, 200) do + tiles = try_dig(consume tiles) + end + + if depth < max_depth then + tiles = add_downstairs(consume tiles) + end + + consume tiles + + fun val try_dig(ts: Tiles iso): Tiles iso^ => + // TODO: Use determinate seed + let guid_gen: GuidGenerator = GuidGenerator() + let guid = guid_gen() + let d = diameter + recover + var tiles: Tiles = consume ts + try + let shapes = tiles.room_shapes() + let shapes_idx = Rand.usize_between(0, tiles.room_shapes().size()) + let shape = shapes(shapes_idx) + let pos_idx = Rand.usize_between(0, shape.perimeter_size()) + let pos = shape.perimeter_space(pos_idx) + + let west = tiles(pos + Directions.left()).is_diggable() + let e = tiles(pos + Directions.right()).is_diggable() + let s = tiles(pos + Directions.down()).is_diggable() + let n = tiles(pos + Directions.up()).is_diggable() + + let h = Rand.i32_between(3, 10) + let w = Rand.i32_between(3, 20) + + if is_blocked(tiles, pos) then + None + elseif e then + let offset = Rand.i32_between(-(h / 2), -1) + tiles = try_room(tiles, pos, pos + Directions.right(), + Pos(0, offset), h, w, guid) + elseif west then + let offset = Rand.i32_between(-(h / 2), -1) + tiles = try_room(tiles, pos, pos + Directions.left(), + Pos(-(w - 1), offset), h, w, guid) + elseif s then + let offset = Rand.i32_between(-(w / 2), -1) + tiles = try_room(tiles, pos, pos + Directions.down(), + Pos(offset, 0), h, w, guid) + elseif n then + let offset = Rand.i32_between(-(w / 2), -1) + tiles = try_room(tiles, pos, pos + Directions.up(), + Pos(offset, -(h - 1)), h, w, guid) + end + end + consume tiles + end + + fun tag is_blocked(tiles: Tiles, pos: Pos val): Bool => + (exists_and_is_blocked(tiles, pos + Directions.up()) + and exists_and_is_blocked(tiles, pos + Directions.left())) + or (exists_and_is_blocked(tiles, pos + Directions.up()) + and exists_and_is_blocked(tiles, pos + Directions.right())) + or (exists_and_is_blocked(tiles, pos + Directions.down()) + and exists_and_is_blocked(tiles, pos + Directions.left())) + or (exists_and_is_blocked(tiles, pos + Directions.down()) + and exists_and_is_blocked(tiles, pos + Directions.right())) + + fun tag exists_and_is_blocked(tiles: Tiles, pos: Pos val): Bool => + try + (not tiles(pos).is_passable()) and (not tiles(pos).is_diggable()) + else + true + end + + fun tag try_room(tiles: Tiles, door: Pos val, connector: Pos val, + offset: Pos val, h: I32, w: I32, guid: U128): Tiles + => + let top_left = connector + offset + let scan = Scan(top_left, h, w) + // Check for free space + try + for p in scan() do + if not tiles(p).is_diggable() then return tiles end + end + let next_shape = RectRoom(top_left, Pos(top_left.x + (w - 1), + top_left.y + (h - 1))) + tiles.add_room(guid, next_shape) + for p in next_shape.perimeter() do + try tiles(p) = + recover + Tile(EmptyOccupant, OccupantCodes.none(), Wall, -1 + where r_id = guid) + end + end + end + for p in next_shape.interior() do + try tiles(p) = + recover + Tile(EmptyOccupant, OccupantCodes.none(), Floor, -1 + where r_id = guid) + end + end + end + tiles(connector) = Tile(EmptyOccupant, OccupantCodes.none(), Floor, -1 + where r_id = guid) + + let last_r_id = tiles(door).room_id + tiles(door) = Tile(EmptyOccupant, OccupantCodes.none(), Floor, -1 + where r_id = last_r_id) + end + tiles + + fun tag add_downstairs(ts: Tiles iso): Tiles iso^ => + recover + let tiles: Tiles = consume ts + let room_count = tiles.room_count() + let room_idx = Rand.usize_between(0, room_count - 1) + try + let room = tiles.room(room_idx) + let pos = room.rand_interior_position() + tiles(pos).update_landmark(DownStairs(DungeonBuilder)) + end + tiles + end diff --git a/src/generators/simple-tiles.pony b/src/generators/simple-tiles.pony new file mode 100755 index 0000000..b0a34c3 --- /dev/null +++ b/src/generators/simple-tiles.pony @@ -0,0 +1,8 @@ +use "../world" + +primitive SimpleTiles + fun apply(diameter: I32): Tiles iso^ => + recover + Tiles(diameter, diameter, {(): Tile => + Tile(EmptyOccupant, OccupantCodes.none(), Floor)}) + end diff --git a/src/guid/guid.pony b/src/guid/guid.pony new file mode 100755 index 0000000..ec465ca --- /dev/null +++ b/src/guid/guid.pony @@ -0,0 +1,11 @@ +use "random" +use "time" + +class GuidGenerator + let _rand: Random + + new create(seed: U64 = Time.micros()) => + _rand = MT(seed) + + fun ref apply(): U128 => + _rand.u128() diff --git a/src/help/help.pony b/src/help/help.pony new file mode 100755 index 0000000..3140320 --- /dev/null +++ b/src/help/help.pony @@ -0,0 +1,31 @@ +primitive Help + fun apply(): Array[String] => + ///////////////////////////////////////////////////////////////// + recover [ + "***************HELP (press h or esc to return)***************", + "", + "NORMAL MODE (moving around map):", + " - movement / attack ", + ". - wait (turn passes without action)", + "i - enter INVENTORY MODE", + "l - enter LOOK MODE (inspect tiles from a distance)", + "v - enter VIEW MODE (jump around map)", + "t - (t)ake item on tile", + "> - descend stairs", + "< - ascend stairs", + " - inspect tile you're on (and see item type)", + "q - quit", + "", + "INVENTORY MODE:", + " - move through items", + " - equip/drink/use", + "l - (l)ook at item", + "d - (d)rop item", + "i/ - return to NORMAL MODE", + "", + "LOOK MODE/VIEW MODE:", + " - look around", + " - inspect tile (in LOOK MODE)", + " - return to NORMAL MODE", + "**************************************************************" + ] end diff --git a/src/input/commands.pony b/src/input/commands.pony new file mode 100755 index 0000000..73a73c0 --- /dev/null +++ b/src/input/commands.pony @@ -0,0 +1,56 @@ +trait Cmd + fun string(): String + +primitive EmptyCmd is Cmd + fun string(): String => "EmptyCmd" + +primitive ACmd is Cmd + fun string(): String => "ACmd" +primitive ECmd is Cmd + fun string(): String => "ECmd" +primitive NCmd is Cmd + fun string(): String => "NCmd" +primitive UCmd is Cmd + fun string(): String => "UCmd" +primitive YCmd is Cmd + fun string(): String => "YCmd" + +primitive UpCmd is Cmd + fun string(): String => "UpCmd" +primitive DownCmd is Cmd + fun string(): String => "DownCmd" +primitive LeftCmd is Cmd + fun string(): String => "LeftCmd" +primitive RightCmd is Cmd + fun string(): String => "RightCmd" + +primitive UpStairsCmd is Cmd + fun string(): String => "UpStairsCmd" +primitive DownStairsCmd is Cmd + fun string(): String => "DownStairsCmd" + +primitive WaitCmd is Cmd + fun string(): String => "WaitCmd" +primitive InventoryModeCmd is Cmd + fun string(): String => "InventoryModeCmd" +primitive DropCmd is Cmd + fun string(): String => "DropCmd" +primitive TakeCmd is Cmd + fun string(): String => "TakeCmd" +primitive LookCmd is Cmd + fun string(): String => "LookCmd" +primitive HelpCmd is Cmd + fun string(): String => "HelpCmd" +primitive EnterCmd is Cmd + fun string(): String => "EnterCmd" +primitive FastCmd is Cmd + fun string(): String => "FastCmd" +primitive ViewCmd is Cmd + fun string(): String => "ViewCmd" +primitive EscCmd is Cmd + fun string(): String => "EscCmd" +primitive ResetCmd is Cmd + fun string(): String => "ResetCmd" +primitive QuitCmd is Cmd + fun string(): String => "QuitCmd" + diff --git a/src/input/input.pony b/src/input/input.pony new file mode 100755 index 0000000..da0c9aa --- /dev/null +++ b/src/input/input.pony @@ -0,0 +1,74 @@ +use "term" +use "../game" +use "../datast" +use "../display" + +class InputNotify + """ + Receive input from an ANSITerm. + """ + var _initialized: Bool = false + let _game: Game + + new iso create(game: Game) => + _game = game + + fun is_recognized(cmd: Cmd val): Bool => + cmd isnt EmptyCmd + + fun ref apply(term: ANSITerm ref, input: U8) => + let cmd = KeyTranslator(input) + if is_recognized(cmd) then + _game(cmd) + end + + fun ref up(ctrl: Bool, alt: Bool, shift: Bool) => + _game(UpCmd) + + fun ref down(ctrl: Bool, alt: Bool, shift: Bool) => + _game(DownCmd) + + fun ref left(ctrl: Bool, alt: Bool, shift: Bool) => + _game(LeftCmd) + + fun ref right(ctrl: Bool, alt: Bool, shift: Bool) => + _game(RightCmd) + + fun ref delete(ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref insert(ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref home(ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref end_key(ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref page_up(ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref page_down(ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref fn_key(i: U8, ctrl: Bool, alt: Bool, shift: Bool) => + None + + fun ref prompt(term: ANSITerm ref, value: String) => + None + + fun ref size(rows: U16, cols: U16) => + if (rows < 31) or (cols < 99) then + ifdef not windows then + _game.fail_term_too_small() + end + else + if not _initialized then + _game.start() + _initialized = true + end + end + + fun ref closed() => + None diff --git a/src/input/key-translator.pony b/src/input/key-translator.pony new file mode 100755 index 0000000..7fea2da --- /dev/null +++ b/src/input/key-translator.pony @@ -0,0 +1,31 @@ +primitive KeyTranslator + fun apply(ch: U8): Cmd val => + match ch + | Keys.left() => LeftCmd + | Keys.right() => RightCmd + | Keys.up() => UpCmd + | Keys.down() => DownCmd + | Keys.enter() => EnterCmd + | Keys.escape() => EscCmd + + | 'a' => ACmd + | 'e' => ECmd + | 'n' => NCmd + | 'u' => UCmd + | 'y' => YCmd + + | 'd' => DropCmd + | 'f' => FastCmd + | 'h' | '?' => HelpCmd + | 'i' => InventoryModeCmd + | 'l' => LookCmd + | 'r' => ResetCmd + | 't' => TakeCmd + | 'v' => ViewCmd + | '.' => WaitCmd + | '<' => UpStairsCmd + | '>' => DownStairsCmd + | 'q' | 'Q' => QuitCmd + else + EmptyCmd + end diff --git a/src/input/keys.pony b/src/input/keys.pony new file mode 100755 index 0000000..f87316b --- /dev/null +++ b/src/input/keys.pony @@ -0,0 +1,9 @@ +// OSX +primitive Keys + fun down(): U8 => 66 + fun up(): U8 => 65 + fun left(): U8 => 68 + fun right(): U8 => 67 + fun backspace(): U8 => 127 + fun escape(): U8 => 27 + fun enter(): U8 => 13 diff --git a/src/invariant/invariant.pony b/src/invariant/invariant.pony new file mode 100755 index 0000000..38670db --- /dev/null +++ b/src/invariant/invariant.pony @@ -0,0 +1,41 @@ +primitive Invariant is _InvariantFailure + """ + This is a debug only assertion. If the test is false, it will print + 'Invariant violated' along with the source file location of the + invariant to stderr and then forcibly exit the program. + """ + fun apply(test: Bool, loc: SourceLoc = __loc) => + ifdef debug then + if not test then + fail(loc) + end + end + +primitive LazyInvariant is _InvariantFailure + """ + This is a debug only assertion. If the test is false or throws an error, + it will print 'Invariant violated' along with the source file location + of the invariant to stderr and then forcibly exit the program. + """ + fun apply(f: {(): Bool ?}, loc: SourceLoc = __loc) => + ifdef debug then + try + if not f() then + fail(loc) + end + else + fail(loc) + end + end + +trait _InvariantFailure + """ + Common failure handling scheme for invariants. + """ + fun fail(loc: SourceLoc) => + @fprintf[I32]( + @pony_os_stderr[Pointer[U8]](), + "Invariant violated in %s at line %s\n".cstring(), + loc.file().cstring(), + loc.line().string().cstring()) + @exit[None](U8(1)) diff --git a/src/inventory/equipment.pony b/src/inventory/equipment.pony new file mode 100755 index 0000000..9fbea59 --- /dev/null +++ b/src/inventory/equipment.pony @@ -0,0 +1,483 @@ +use "../agents" +use "../display" + +trait Item is Equatable[Item] + fun weight(): I32 + fun cost(): I32 + fun name(): String + fun description(): String => "a " + name() + fun string(): String => name() + fun eq(that: box->Item): Bool => name() == that.name() + + fun freeze(): Item val + fun unfreeze(): Item + +class Gold is Item + let _amount: I32 + let _weight: I32 + let _cost: I32 + + new create(a: I32) => + _amount = a + _weight = _amount + _cost = _amount + + fun amount(): I32 => _amount + fun weight(): I32 => _weight + fun cost(): I32 => _cost + fun name(): String => _amount.string() + " gold pieces" + fun description(): String => name() + + fun freeze(): Item val => + let amt = _amount + recover val Gold(amt) end + + fun unfreeze(): Item => + Gold(_amount) + +class GoldBuilder + fun apply(amt: I32): Item val => + recover val Gold(amt) end + +class MiscItem is Item + let _weight: I32 + let _cost: I32 + let _name: String + let _description: String + let _try_to_use_message: String + + new create(n: String, w: I32, c: I32, desc: String = "", + try_to_use_message: String = "") + => + _weight = w + _cost = c + _name = n + _description = if desc == "" then "a " + _name else desc end + _try_to_use_message = try_to_use_message + + fun weight(): I32 => _weight + fun cost(): I32 => _cost + fun name(): String => _name + fun description(): String => _description + fun try_to_use(display: Display tag) => + display.log(_try_to_use_message) + + fun freeze(): Item val => + let w = _weight + let c = _cost + let n = _name + let d = _description + let t = _try_to_use_message + recover MiscItem(n, w, c, d, t) end + + fun unfreeze(): Item => + MiscItem(_name, _weight, _cost, _description, _try_to_use_message) + +class Potion is Item + let _heal: I32 + var _empty: Bool + + new create(h: I32, empty: Bool = false) => + _heal = h + _empty = empty + + fun ref drink(self: Self tag, display: Display tag) => + if not _empty then + display.log("You drink the potion!") + self.drink_potion(_heal) + self.display_stats() + _empty = true + else + display.log("The potion is empty!") + end + + fun weight(): I32 => 2 + fun cost(): I32 => 10 + fun name(): String => if _empty then "empty potion" else "potion" end + fun description(): String => + if _empty then "an empty potion" else "a potion" end + + fun freeze(): Item val => + let h = _heal + let e = _empty + recover Potion(h, e) end + + fun unfreeze(): Item => + Potion(_heal, _empty) + +class Weapon is Item + let _weight: I32 + let _cost: I32 + let _base_name: String + let _base_desc: (String, String) + let _name: String + let _dmg: I32 + let _hands: I8 + let _bonus: I32 + + new create(n: String, d: I32, h: I8, w: I32, c: I32, b: I32 = 0, + desc: (String, String) = ("", "")) + => + _weight = w + _cost = c + _bonus = b + _base_name = n + _base_desc = desc + _name = + if _bonus > 0 then n + " +" + _bonus.string() else n end + _dmg = d + _hands = h + + fun weight(): I32 => _weight + fun cost(): I32 => _cost + fun dmg(): I32 => _dmg + fun hands(): I8 => _hands + fun bonus(): I32 => _bonus + fun name(): String => _name + fun description(): String => + if (_base_desc._1 == "") and (_base_desc._2 == "") then name() + else _base_desc._1 + " " + name() + " " + _base_desc._2 end + + fun freeze(): Item val => + let w = _weight + let c = _cost + let n = _base_name + let d = _dmg + let h = _hands + let b = _bonus + let desc = _base_desc + recover Weapon(n, d, h, w, c, b, desc) end + + fun unfreeze(): Item => + Weapon(_base_name, _dmg, _hands, _weight, _cost, _bonus, _base_desc) + + fun enhance(enhancement: I32): Item val => + let w = _weight + let c = _cost + let n = _base_name + let d = _dmg + let h = _hands + let b = enhancement + let desc = _base_desc + recover Weapon(n, d, h, w, c, b, desc) end + +class Armor is Item + let _weight: I32 + let _cost: I32 + let _base_name: String + let _base_desc: (String, String) + let _name: String + let _ac: I32 + let _bonus: I32 + let _armor_type: ArmorType + + new create(n: String, ac': I32, b: I32, at: ArmorType, w: I32, c: I32, + desc: (String, String) = ("", "")) => + _weight = w + _cost = c + _bonus = b + _base_name = n + _base_desc = desc + _name = + if _bonus > 0 then n + " +" + _bonus.string() else n end + _ac = ac' + _armor_type = at + + fun weight(): I32 => _weight + fun cost(): I32 => _cost + fun ac(): I32 => _ac + _bonus + fun bonus(): I32 => _bonus + fun armor_type(): ArmorType => _armor_type + fun name(): String => _name + fun description(): String => + if (_base_desc._1 == "") and (_base_desc._2 == "") then name() + else _base_desc._1 + " " + name() + " " + _base_desc._2 end + + fun freeze(): Item val => + let w = _weight + let c = _cost + let n = _base_name + let d = _base_desc + let ac' = _ac + let b = _bonus + let at = _armor_type + recover Armor(n, ac', b, at, w, c, d) end + + fun unfreeze(): Item => + Armor(_base_name, _ac, _bonus, _armor_type, _weight, _cost, _base_desc) + + fun enhance(enhancement: I32): Item val => + let w = _weight + let c = _cost + let n = _base_name + let ac' = _ac + let b = enhancement + let at = _armor_type + let d = _base_desc + recover Armor(n, ac', b, at, w, c, d) end + +type ArmorType is ( + BodyArmor | + Shield | + Helmet +) + +primitive BodyArmor +primitive Shield +primitive Helmet + +interface ItemBuilder + fun apply(bonus: I32 = 0): Item val + +/////////////////// +// Armor +/////////////////// +primitive PaddedClothArmorBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Padded Cloth Armor", 1, bonus, BodyArmor, 10, 100) end + +primitive LeatherArmorBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Leather Armor", 2, bonus, BodyArmor, 15, 200) end + +primitive RingMailBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Ring Mail", 3, bonus, BodyArmor, 25, 300) end + +primitive ScaleMailBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Scale Mail", 4, bonus, BodyArmor, 30, 450) end + +primitive ChainMailBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Chain Mail", 5, bonus, BodyArmor, 40, 600) end + +primitive PlateMailBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Plate Mail", 7, bonus, BodyArmor, 50, 3000) end + +primitive BucklerBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Buckler", 1, bonus, Shield, 5, 70 + where desc = ("a", "")) + end + +primitive GreatShieldBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Armor("Great Shield", 2, bonus, Shield, 10, 350 + where desc = ("a", "")) + end + +/////////////////// +// Weapons +/////////////////// + +//4 +primitive ClubBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Club", 4, 1, 1, 2, bonus) end + +primitive DaggerBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Dagger", 4, 1, 1, 20, bonus) end + +primitive SilverDaggerBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Silver Dagger", 4, 1, 1, 250, 1 + bonus) end + +//6 +primitive QuarterstaffBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Quarterstaff", 6, 1, 4, 20, bonus) end + +primitive HandAxeBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Hand Axe", 6, 1, 5, 40, bonus) end + +primitive ShortSwordBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Short Sword", 6, 1, 3, 60, bonus) end + +primitive LightHammerBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Light Hammer", 6, 1, 5, 40, bonus) end + +//8 +primitive LongSwordBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Long Sword", 8, 1, 4, 100, bonus) end + +primitive BattleAxeBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Battle Axe", 8, 1, 7, 70, bonus) end + +primitive WarhammerBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Warhammer", 8, 1, 10, 60, bonus) end + +primitive MaceBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Mace", 8, 1, 10, 60, bonus) end + +//10 +primitive GreatAxeBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Great Axe", 10, 2, 15, 140, bonus) end + +primitive BroadSwordBuilder + fun apply(bonus: I32 = 0): Item val => + recover val Weapon("Broad Sword", 10, 2, 10, 180, bonus) end + +/////////////////// +// Items +/////////////////// +primitive JarBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("Jar", 1, 10, "an ordinary jar", + "You don't have anything worth storing in there.") end + +primitive BottleBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("Bottle", 1, 10, "an empty bottle", + "There's nothing to drink in there.") end + +primitive BookBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("Book", 1, 10, "a mysterious book", + "You quickly get bored.") end + +primitive StringBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("String", 1, 10, "a piece of string", + "You tie a knot and then untie it.") end + +primitive CardsBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("Deck of Cards", 1, 10, "a deck of cards", + "No time for solitaire.") end + +primitive SoapBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("Soap", 1, 10, "a bar of soap", + "You need water.") end + +primitive BrassBellBuilder + fun apply(bonus: I32 = 0): Item val => + recover val MiscItem("Bell", 1, 10, "a small brass bell", + "Ding.") end + +primitive PotionBuilder + fun apply(heal: I32): Item val => + recover val Potion(heal) end + +/////////////////// +// Victory +/////////////////// +class StaffOfEternity is Item + fun weight(): I32 => 0 + fun cost(): I32 => 0 + fun name(): String => "Staff of Eternity" + fun description(): String => "the " + name() + + fun freeze(): Item val => + recover val StaffOfEternity end + + fun unfreeze(): Item => + StaffOfEternity + +// var armor = { +// none: { +// ac: 0, +// weight: 0, +// cost: 0, +// name: "No Armor", +// armorType: "armor" +// }, + +// }; + +// var weapons = { +// shortbow: { +// //dmg refers to hitting with the bow itself +// dmg: 2, +// weight: 2, +// cost: 250, +// hands: 2, +// range: [50, 100, 150], +// name: "Shortbow" +// }, +// shortbowArrow: { +// dmg: 6, +// weight: 0, +// cost: 1, +// hands: 0, +// name: "Shortbow Arrow" +// }, +// longbow: { +// //dmg refers to hitting with the bow itself +// dmg: 2, +// weight: 3, +// cost: 60, +// hands: 2, +// range: [70, 140, 210], +// name: "Longbow" +// }, +// longbowArrow: { +// dmg: 8, +// weight: 0, +// cost: 2, +// hands: 0, +// name: "Longbow Arrow" +// }, +// lightCrossbow: { +// //dmg refers to hitting with the bow itself +// dmg: 2, +// weight: 7, +// cost: 300, +// hands: 2, +// range: [60, 120, 180], +// name: "Light Crossbow" +// }, +// lightQuarrel: { +// dmg: 6, +// weight: 0, +// cost: 2, +// hands: 0, +// name: "Light Quarrel" +// }, +// heavyCrossbow: { +// //dmg refers to hitting with the bow itself +// dmg: 2, +// weight: 14, +// cost: 500, +// hands: 2, +// range: [80, 160, 240], +// name: "Heavy Crossbow" +// }, +// heavyQuarrel: { +// dmg: 8, +// weight: 0, +// cost: 4, +// hands: 0, +// name: "Heavy Quarrel" +// }, +// }; + +// var items = { +// book: { +// weight: 1, +// cost: 1, +// name: "Book" +// } +// }; + +// var lightSources = { +// torch: { +// weight: 1, +// cost: 1, +// name: "Torch", +// lightDiameter: 10, +// duration: 500 +// } +// }; +// }); diff --git a/src/inventory/inventory.pony b/src/inventory/inventory.pony new file mode 100755 index 0000000..7ebf9dd --- /dev/null +++ b/src/inventory/inventory.pony @@ -0,0 +1,410 @@ +use "collections" +use "../agents" +use "../datast" +use "../display" +use "../world" + +class Inventory + let _max_size: USize = 40 + let _armor: Array[Item] = Array[Item] + let _misc: Array[Item] = Array[Item] + let _potions: Array[Item] = Array[Item] + let _weapons: Array[Item] = Array[Item] + + new create() => + _armor.push(RingMailBuilder().unfreeze()) + _misc.push(JarBuilder().unfreeze()) + _weapons.push(ShortSwordBuilder().unfreeze()) + + fun apply(idx: USize): this->Item ? => + let w = _weapons.size() + let a = _armor.size() + let p = _potions.size() + let m = _misc.size() + + if idx < w then + _weapons(idx) + elseif idx < (w + a) then + _armor(idx - w) + elseif idx < (w + a + p) then + _potions(idx - (w + a)) + else + _misc(idx - (w + a + p)) + end + + fun size(): USize => + _armor.size() + _misc.size() + _potions.size() + _weapons.size() + + fun can_add(): Bool => size() < _max_size + + fun weapons_starting_idx(): I32 => + 0 + + fun armor_starting_idx(): I32 => + _weapons.size().i32() + + fun potions_starting_idx(): I32 => + armor_starting_idx() + _armor.size().i32() + + fun misc_starting_idx(): I32 => + potions_starting_idx() + _potions.size().i32() + + fun ref add(item: Item): (I32 | None) => + // Return index of new item if it's added + if can_add() then + match item + | let w: Weapon => + _weapons.push(w) + _weapons.size().i32() - 1 + | let a: Armor => + _armor.push(a) + armor_starting_idx() + (_armor.size() - 1).i32() + | let p: Potion => + _potions.push(p) + potions_starting_idx() + (_potions.size() - 1).i32() + else + _misc.push(item) + misc_starting_idx() + (_misc.size() - 1).i32() + end + else + None + end + + fun ref remove(idx: USize): (Item | None) => + let w = _weapons.size() + let a = _armor.size() + let p = _potions.size() + let m = _misc.size() + + try + if idx < w then + let i = _weapons(idx) + _weapons.remove(idx, 1) + return i + elseif idx < (w + a) then + let i = _armor(idx - w) + _armor.remove(idx - w, 1) + return i + elseif idx < (w + a + p) then + let i = _potions(idx - (w + a)) + _potions.remove(idx - (w + a), 1) + return i + else + let i = _misc(idx - (w + a + p)) + _misc.remove(idx - (w + a + p), 1) + return i + end + else + None + end + + fun category(idx: USize): String => + let w = _weapons.size() + let a = _armor.size() + let p = _potions.size() + let m = _misc.size() + + if idx < w then "Weapons" + elseif idx < (w + a) then "Armor" + elseif idx < (w + a + p) then "Potions" + else "Miscellaneous" end + + fun local_idx(idx: USize): USize => + let w = _weapons.size() + let a = _armor.size() + let p = _potions.size() + let m = _misc.size() + + if idx < w then idx + elseif idx < (w + a) then idx - w + elseif idx < (w + a + p) then idx - (w + a) + else idx - (w + a + p) end + + fun displayable(): Map[String, Array[String] val] val => + let map: Map[String, Array[String] val] trn = + recover Map[String, Array[String] val] end + map("Armor") = _string_array(_armor) + map("Miscellaneous") = _string_array(_misc) + map("Potions") = _string_array(_potions) + map("Weapons") = _string_array(_weapons) + consume map + + fun _string_array(items: Array[Item] box): Array[String] iso^ => + let arr: Array[String] iso = recover Array[String] end + for item in items.values() do + arr.push(item.string()) + end + consume arr + +class InventoryManager + let _inventory: Inventory + let _self: Self tag + let _agent_data: AgentData + let _display: Display tag + var _current: I32 = 0 + var _weapon: (Weapon | None) = None + var _armor: (Armor | None) = None + var _helmet: (Armor | None) = None + var _shield: (Armor | None) = None + //One slot for each of [weapon, armor, helmet, shield] + //-1 if nothing is equipped + let _equipped: Array[I32] = Array[I32] + + new create(inventory: Inventory, self: Self tag, agent_data: AgentData, + display: Display tag) => + _inventory = inventory + _self = self + _agent_data = agent_data + _display = display + _equipped.push(-1) + _equipped.push(-1) + _equipped.push(-1) + _equipped.push(-1) + _init() + + fun ref _init() => + equip(); next(); equip() + _current = 0 + + fun ref next() => + _current = (_current + 1) % _inventory.size().i32() + + fun ref prev() => + _current = _current - 1 + if _current < 0 then _current = (_inventory.size() - 1).i32() end + + fun ref reset_current() => _current = 0 + + fun description(): String => + try + _inventory(_current.usize()).description() + else + "nothing" + end + + fun ref drop(w: World tag, pos: Pos val) => + let item = _remove_and_unequip() + match item + | let i: Item => + w.try_add_item(i.freeze(), pos) + end + + fun ref destroy() => + _remove_and_unequip() + + fun ref _remove_and_unequip(): (Item | None) => + let item = _inventory.remove(_current.usize()) + match item + | let i: Item => + _unequip(i) + for (idx, v) in _equipped.pairs() do + try + if v == _current then + _equipped(idx) = -1 + elseif v > _current then + _equipped(idx) = v - 1 + end + end + end + if _current.usize() >= _inventory.size() then _current = _current - 1 end + if _current < 0 then _current = 0 end + i + else + None + end + + fun ref add(item: Item val): Bool => + match item + | let s: StaffOfEternity val => + _display.log("You have found the Staff of Eternity!") + _self.win_game() + true + | let gp: Gold val => + _agent_data.modify_gp(gp.amount()) + true + else + match _inventory.add(item.unfreeze()) + | let idx: I32 => + for (i, v) in _equipped.pairs() do + try + if v >= idx then + _equipped(i) = v + 1 + end + end + end + true + else + false + end + end + + fun ref try_item() => + try + let item = _inventory(_current.usize()) + match item + | let w: Weapon => equip() + | let a: Armor => equip() + | let p: Potion => utilize() + | let mi: MiscItem => mi.try_to_use(_display) + else + _display.log("You don't know what to do with the " + item.name()) + end + else + _display.log("<>") + end + + fun ref utilize() => + try + let item = _inventory(_current.usize()) + match item + | let p: Potion => + p.drink(_self, _display) + destroy() + | let mi: MiscItem => mi.try_to_use(_display) + else + _display.log("You don't know what to do with the " + item.name()) + end + else + _display.log("<>") + end + + fun ref equip() => + try + let item = _inventory(_current.usize()) + match item + | let w: Weapon => equip_weapon(w) + | let a: Armor => equip_armor(a) + else + _display.log("The " + item.name() + " cannot be equipped.") + end + else + _display.log("<>") + end + + fun ref _unequip(i: Item) => + match i + | let w: Weapon => unequip_weapon(w) + | let a: Armor => unequip_armor(a) + end + + fun ref equip_weapon(w: Weapon) => + try + let old_w = _weapon as Weapon + _agent_data.modify_hit_bonus(-old_w.bonus()) + end + _weapon = w + _agent_data.update_dmg(w.dmg()) + _agent_data.update_dmg_bonus(w.bonus()) + _agent_data.modify_hit_bonus(w.bonus()) + try _equipped(0) = _current end + + fun ref equip_armor(a: Armor) => + match a.armor_type() + | BodyArmor => + match _armor + | let ar: Armor => + _agent_data.modify_ac(-ar.ac()) + end + _armor = a + _agent_data.modify_ac(a.ac()) + try _equipped(1) = _current end + | Helmet => + match _helmet + | let ar: Armor => + _agent_data.modify_ac(-ar.ac()) + end + _helmet = a + _agent_data.modify_ac(a.ac()) + try _equipped(2) = _current end + | Shield => + match _shield + | let ar: Armor => + _agent_data.modify_ac(-ar.ac()) + end + _shield = a + _agent_data.modify_ac(a.ac()) + try _equipped(3) = _current end + end + + fun ref unequip_weapon(w: Weapon) => + try + if _equipped(0) == _current then + _agent_data.modify_hit_bonus(-w.bonus()) + _agent_data.update_dmg(1) + _agent_data.update_dmg_bonus(0) + _equipped(0) = -1 + end + end + + fun ref unequip_armor(a: Armor) => + try + match a.armor_type() + | BodyArmor => + if _equipped(1) == _current then + match _armor + | let ar: Armor => + _agent_data.modify_ac(-ar.ac()) + end + try _equipped(1) = -1 end + end + | Helmet => + if _equipped(2) == _current then + match _helmet + | let ar: Armor => + _agent_data.modify_ac(-ar.ac()) + end + try _equipped(2) = -1 end + end + | Shield => + if _equipped(3) == _current then + match _shield + | let ar: Armor => + _agent_data.modify_ac(-ar.ac()) + try _equipped(3) = -1 end + end + end + end + end + + fun displayable(): InventoryDisplayable val => + let equipped: Map[String, Array[USize] val] trn = + recover Map[String, Array[USize] val] end + equipped("Armor") = _equipped_list_armor() + equipped("Weapons") = _equipped_list_weapons() + let highlighted = (_inventory.category(_current.usize()), + _inventory.local_idx(_current.usize())) + InventoryDisplayable(highlighted, _inventory.displayable(), + consume equipped) + + fun _equipped_list_armor(): Array[USize] val => + let list: Array[USize] iso = recover Array[USize] end + try + for (idx, v) in _equipped.pairs() do + if (idx > 0) and (idx < 4) and (v != -1) then + list.push(_inventory.local_idx(_equipped(idx).usize())) + end + end + end + consume list + + fun _equipped_list_weapons(): Array[USize] val => + let list: Array[USize] iso = recover Array[USize] end + try + if _equipped(0) != -1 then + list.push(_inventory.local_idx(_equipped(0).usize())) + end + end + consume list + + +class InventoryDisplayable + let highlighted: (String, USize) + let items: Map[String, Array[String] val] val + let equipped: Map[String, Array[USize] val] val + + new val create(highlighted': (String, USize), + items': Map[String, Array[String] val] val, + equipped': Map[String, Array[USize] val] val) => + highlighted = highlighted' + items = items' + equipped = equipped' diff --git a/src/log/logger.pony b/src/log/logger.pony new file mode 100755 index 0000000..fe8e990 --- /dev/null +++ b/src/log/logger.pony @@ -0,0 +1,3 @@ +primitive Logger + fun info(s: String) => @printf[I32](("[INFO] " + s + "\n").cstring()) + fun err(s: String) => @printf[I32](("[ERROR] " + s + "\n").cstring()) diff --git a/src/rand/rand.pony b/src/rand/rand.pony new file mode 100755 index 0000000..f76f22e --- /dev/null +++ b/src/rand/rand.pony @@ -0,0 +1,42 @@ +use "collections" +use "random" +use "time" + +class Rand + let _rand: Random + let _indices: Array[USize] = Array[USize] + + new create(seed: U64 = Time.micros()) => + _rand = MT(seed) + + fun ref flip(): U64 => _rand.int(2) + + fun ref roll(dice: I32, sides: I32): I32 => + var result: I32 = 0 + for i in Range[I32](0, dice) do + result = result + i32_between(1, sides) + end + result + + fun ref i32_between(low: I32, high: I32): I32 => + let r = (_rand.int((high.u64() + 1) - low.u64()) and 0x0000FFFF).i32() + let value = r + low + value + + fun ref usize_between(low: USize, high: USize): USize => + let r = (_rand.int((high.u64() + 1) - low.u64()) and 0x0000FFFF).usize() + let value = r + low + value + + fun ref shuffle_array[V](a: Array[V]): Array[V] ? => + let size = a.size() + _indices.clear() + for i in Range(0, size) do + _indices.push(i) + end + for (i, value) in _indices.pairs() do + let r = usize_between(i + 1, size - 1) + _indices(i) = _indices(r) + end + a.permute(_indices.values()) + a diff --git a/src/rules/rules.pony b/src/rules/rules.pony new file mode 100755 index 0000000..90c5a9e --- /dev/null +++ b/src/rules/rules.pony @@ -0,0 +1,46 @@ +// define(function(require) { + +// var Console = require("js/screens/Console"); + +// var Combat = { +// attack: function(attacker, defender) { +// if (attacker.rollToHit(defender.getArmorClass())) { +// var damage = attacker.rollToDamage(); +// defender.loseHP(damage); +// Console.msg(attacker.describe() + " hits " + defender.describe() + " for " + damage + " damage!"); +// } else { +// Console.msg(attacker.describe() + " misses!"); +// } +// } +// }; + +// return Combat; +// }); + + // rollToHit: function(targetArmorClass) { + // return Rand.roll(20) > targetArmorClass; + // }, + // rollToDamage: function() { + // return this.attackDice.roll() + this.hitBonus; + // }, + // loseHP: function(damage) { + // this.hp -= damage; + // if (this.isDead()) this.fall(); + // }, + +// define(function(require) { + +// var bonuses = { +// 3: -3, +// 5: -2, +// 8: -1, +// 12: 0, +// 15: 1, +// 17: 2, +// 18: 3 +// }; + +// return { +// bonuses: bonuses +// } +// }); \ No newline at end of file diff --git a/src/world/directions.pony b/src/world/directions.pony new file mode 100755 index 0000000..390c31d --- /dev/null +++ b/src/world/directions.pony @@ -0,0 +1,19 @@ +use "../datast" +use "../rand" + +primitive Directions + fun down(): Pos val => Pos(0, 1) + fun up(): Pos val => Pos(0, -1) + fun left(): Pos val => Pos(-1, 0) + fun right(): Pos val => Pos(1, 0) + + fun rand_cardinal(): Pos val => + let roll = Rand.i32_between(0, 3) + match roll + | 0 => left() + | 1 => right() + | 2 => up() + | 3 => down() + else + Pos(0, 0) + end diff --git a/src/world/dungeon.pony b/src/world/dungeon.pony new file mode 100755 index 0000000..aa818e3 --- /dev/null +++ b/src/world/dungeon.pony @@ -0,0 +1,95 @@ +use "collections" +use "random" +use "time" +use "../agents" +use "../datast" +use "../display" +use "../encounters" +use "../game" +use "../generators" +use "../guid" +use "../inventory" +use "../log" + +actor Dungeon is World + let _parent_world: World tag + // TODO: Use determinate seed + let _guid_gen: GuidGenerator = GuidGenerator + let _display: Display tag + let _diameter: I32 + let _tiles: Tiles + let _agents: Agents + let _turn_manager: TurnManager tag + let _depth: I32 + var _last_focus: Pos val + var _present: Bool = false + var _last_turn: I32 = 0 + + new create(d: I32, t_manager: TurnManager tag, display': Display tag, + depth': I32 = 1, parent': World tag = EmptyWorld, + self: (Self | None) = None) + => + _diameter = d + _turn_manager = t_manager + _parent_world = parent' + _depth = depth' + let starting_pos = Pos(_diameter / 2, _diameter / 2) + _last_focus = starting_pos + let ts: Tiles iso = Digout(d, starting_pos, _depth) + _display = display' + _agents = Agents(_display) + _tiles = PerRoomInitialPlacements(consume ts, _depth, _display) + + try + let s = self as Self + PerRoomAgentPlacements(_tiles, this, _turn_manager, _depth, s) + else + PerRoomAgentPlacements(_tiles, this, _turn_manager, _depth) + end + + if _depth > 1 then + try _tiles(starting_pos).update_landmark(UpStairs) end + end + + be increment_turn() => _last_turn = _last_turn + 1 + + be enter(self: Self) => + add_agent(self, _last_focus, OccupantCodes.self()) + self.update_world(this) + self.enter_world_at(_last_focus, _depth) + _display.log("Entering depth " + _depth.string()) + _present = true + + fun ref _exit(pos: Pos val, self: Self) => + _last_focus = pos + remove_occupant(pos) + agents().remove(self) + _present = false + + be add_agent(a: Agent tag, pos: Pos val, occupant_code: I32) => + _agents.add(a) + set_occupant(pos, a, occupant_code) + + fun display(): Display tag => _display + + fun diameter(): I32 => _diameter + + fun ref tile(pos: Pos val): Tile => + try + _tiles(pos) + else + Tile.empty() + end + + fun ref tiles(): Tiles => _tiles + + fun ref agents(): Agents => _agents + + fun depth(): I32 => _depth + + fun parent(): World tag => _parent_world + + fun turn_manager(): (TurnManager | None) => _turn_manager + + fun present(): Bool => _present + diff --git a/src/world/landmark.pony b/src/world/landmark.pony new file mode 100755 index 0000000..b9d401e --- /dev/null +++ b/src/world/landmark.pony @@ -0,0 +1,41 @@ +use "../agents" +use "../datast" +use "../display" +use "../game" + +type Landmark is ( + UpStairs val | + DownStairs val | + EmptyLandmark val +) + +primitive UpStairs + fun description(): String => "stairs leading up" + +class DownStairs + var world: World tag + let _world_builder: WorldBuilder val + + new val create(wb: WorldBuilder val, + world': World tag = EmptyWorld) + => + world = world' + _world_builder = wb + + fun is_initialized(): Bool => + match world + | let ew: EmptyWorld => false + else true + end + + fun build_world(diameter: I32, turn_manager: TurnManager tag, + display: Display tag, depth: I32, parent: World tag): DownStairs val + => + let w = _world_builder(diameter, turn_manager, display, depth, + parent) + DownStairs(_world_builder, w) + + fun description(): String => "stairs leading down" + +primitive EmptyLandmark + fun description(): String => "" diff --git a/src/world/line-of-sight.pony b/src/world/line-of-sight.pony new file mode 100755 index 0000000..205746e --- /dev/null +++ b/src/world/line-of-sight.pony @@ -0,0 +1,124 @@ +use "../datast" + +primitive LineOfSight + fun apply(pos1: Pos val, pos2: Pos val, ts: Tiles iso): Bool => + let tiles: Tiles = consume ts + let arr: Array[Pos val] iso = recover Array[Pos val] end + let iter = LineIterator(pos1, pos2) + for pos in iter do + try + let t = tiles(pos) + if t.is_transparent() then + if pos == pos1 then return true end + else + if pos == pos1 then return true end + return false + end + end + end + false + + fun visible_along(pos1: Pos val, pos2: Pos val, ts: Tiles iso): + Seq[Pos val] + => + let tiles: Tiles = consume ts + let arr: Array[Pos val] iso = recover Array[Pos val] end + let iter = LineIterator(pos1, pos2) + for pos in iter do + try + let t = tiles(pos) + if t.is_transparent() then + arr.push(pos) + else + arr.push(pos) + break + end + end + end + consume arr + +class LineIterator is Iterator[Pos val] + var _idx: USize = 0 + let _pos1: Pos val + let _pos2: Pos val + let _output_pos_builder: {(I32, I32): Pos val} val + var _dx: I32 + var _dy: I32 + var _d: I32 + var _x: I32 + var _y: I32 + var _done: Bool = false + + new create(pos1: Pos val, pos2: Pos val) => + let octant = Octants.find_octant(pos1, pos2) + let input_pos_builder = Octants.input_pos_builder(octant) + _pos1 = input_pos_builder(pos1.x, pos1.y) + _pos2 = input_pos_builder(pos2.x, pos2.y) + _output_pos_builder = Octants.output_pos_builder(octant) + _dx = _pos2.x - _pos1.x + _dy = _pos2.y - _pos1.y + _d = _dy - _dx + _x = _pos1.x + _y = _pos1.y + + fun ref has_next(): Bool => not _done + + fun ref next(): Pos val => + let n = _output_pos_builder(_x, _y) + if (_x == _pos2.x) and (_y == _pos2.y) then _done = true end + if _d >= 0 then + _y = _y + 1 + _d = _d - _dx + end + _d = _d + _dy + _x = _x + 1 + n + +primitive Octants + fun find_octant(pos1: Pos val, pos2: Pos val): I32 => + let dx = pos2.x - pos1.x + let dy = pos2.y - pos1.y + + // Octant 0-1 + if (dx >= 0) and (dy >= 0) then + if (dx - dy) >= 0 then 0 else 1 end + // Octant 2-3 + elseif (dx < 0) and (dy >= 0) then + if (-dx - dy) <= 0 then 2 else 3 end + // Octant 4-5 + elseif (dx < 0) and (dy < 0) then + if (-dx + dy) >= 0 then 4 else 5 end + // Octant 6-7 + elseif (dx >= 0) and (dy < 0) then + if (dx + dy) <= 0 then 6 else 7 end + else + 0 + end + + fun input_pos_builder(octant: I32): {(I32, I32): Pos val} val => + match octant + | 0 => {(x: I32, y: I32): Pos val => Pos(x, y)} + | 1 => {(x: I32, y: I32): Pos val => Pos(y, x)} + | 2 => {(x: I32, y: I32): Pos val => Pos(y, -x)} + | 3 => {(x: I32, y: I32): Pos val => Pos(-x, y)} + | 4 => {(x: I32, y: I32): Pos val => Pos(-x, -y)} + | 5 => {(x: I32, y: I32): Pos val => Pos(-y, -x)} + | 6 => {(x: I32, y: I32): Pos val => Pos(-y, x)} + | 7 => {(x: I32, y: I32): Pos val => Pos(x, -y)} + else + {(x: I32, y: I32): Pos val => Pos(x, y)} + end + + fun output_pos_builder(octant: I32): {(I32, I32): Pos val} val => + match octant + | 0 => {(x: I32, y: I32): Pos val => Pos(x, y)} + | 1 => {(x: I32, y: I32): Pos val => Pos(y, x)} + | 2 => {(x: I32, y: I32): Pos val => Pos(-y, x)} + | 3 => {(x: I32, y: I32): Pos val => Pos(-x, y)} + | 4 => {(x: I32, y: I32): Pos val => Pos(-x, -y)} + | 5 => {(x: I32, y: I32): Pos val => Pos(-y, -x)} + | 6 => {(x: I32, y: I32): Pos val => Pos(y, -x)} + | 7 => {(x: I32, y: I32): Pos val => Pos(x, -y)} + else + {(x: I32, y: I32): Pos val => Pos(x, y)} + end diff --git a/src/world/looker.pony b/src/world/looker.pony new file mode 100755 index 0000000..eb40788 --- /dev/null +++ b/src/world/looker.pony @@ -0,0 +1,86 @@ +use "../datast" +use "../display" +use "../input" + +// Used to freely look around and inspect map with cursor in LookMode. +actor Looker + var _world: World tag + let _display: Display tag + var _h: I32 + var _w: I32 + var _pos: Pos val = Pos(0, 0) + var _focus: Pos val = Pos(0, 0) + var _min_x: I32 = 0 + var _max_x: I32 = 0 + var _min_y: I32 = 0 + var _max_y: I32 = 0 + + new create(w: World tag, focus: Pos val, display_h: I32, display_w: I32, + display: Display tag) + => + _world = w + _display = display + _h = display_h + _w = display_w + set_coords(focus) + + be update_world(w: World tag) => + _world = w + + be init(h: I32, w: I32, pos: Pos val, world: World tag) => + _h = h + _w = w + _world = world + set_coords(pos) + _world.highlight(_pos) + _world.display_map(h, w, _focus, _display) + + fun ref set_coords(pos: Pos val) => + _pos = pos + _focus = pos + let x = _pos.x + let y = _pos.y + _min_x = (x - (_w / 2)) - 1 + _max_x = (x + (_w / 2)) + 1 + _min_y = (y - (_h / 2)) - 1 + _max_y = (y + (_h / 2)) + 1 + + be close(h: I32, w: I32, focus: Pos val) => + _world.unhighlight(_pos) + _world.display_map(h, w, focus, _display) + + be apply(cmd: Cmd val) => + match cmd + | LeftCmd => + let try_pos = _pos + Directions.left() + if try_pos.x > _min_x then + _world.unhighlight(_pos) + _pos = try_pos + _world.highlight(_pos) + end + | RightCmd => + let try_pos = _pos + Directions.right() + if try_pos.x < _max_x then + _world.unhighlight(_pos) + _pos = try_pos + _world.highlight(_pos) + end + | UpCmd => + let try_pos = _pos + Directions.up() + if try_pos.y > _min_y then + _world.unhighlight(_pos) + _pos = try_pos + _world.highlight(_pos) + end + | DownCmd => + let try_pos = _pos + Directions.down() + if try_pos.y < _max_y then + _world.unhighlight(_pos) + _pos = try_pos + _world.highlight(_pos) + end + | EnterCmd => + _world.describe(_pos, _display) + end + _world.display_map(_h, _w, _focus, _display) + diff --git a/src/world/map_viewer.pony b/src/world/map_viewer.pony new file mode 100755 index 0000000..d26b246 --- /dev/null +++ b/src/world/map_viewer.pony @@ -0,0 +1,59 @@ +use "../datast" +use "../display" +use "../input" + +// Used to look around map by jumping screens in MapViewMode. +actor MapViewer + var _world: World tag + let _display: Display tag + let _h: I32 + let _h_jump: I32 + let _w: I32 + let _w_jump: I32 + var _focus: Pos val = Pos(0, 0) + + new create(w: World tag, focus: Pos val, display_h: I32, display_w: I32, + display: Display tag) + => + _world = w + _display = display + _h = display_h + _h_jump = _h / 4 + _w = display_w + _w_jump = _w / 4 + + be update_world(w: World tag) => + _world = w + + be init(pos: Pos val, world: World tag) => + _world = world + _focus = pos + _world.display_map(_h, _w, _focus, _display) + + be close(focus: Pos val) => + _world.display_map(_h, _w, focus, _display) + + be apply(cmd: Cmd val) => + match cmd + | LeftCmd => + let old = _focus + _focus = _focus + (Directions.left() * _w_jump) + _display_from(old, _focus) + | RightCmd => + let old = _focus + _focus = _focus + (Directions.right() * _w_jump) + _display_from(old, _focus) + | UpCmd => + let old = _focus + _focus = _focus + (Directions.up() * _h_jump) + _display_from(old, _focus) + | DownCmd => + let old = _focus + _focus = _focus + (Directions.down() * _h_jump) + _display_from(old, _focus) + end + + fun _display_from(origin: Pos val, target: Pos val) => + for pos in PosStraightIterator(origin, target) do + _world.display_map(_h, _w, pos, _display) + end diff --git a/src/world/occupant-codes.pony b/src/world/occupant-codes.pony new file mode 100755 index 0000000..2470de6 --- /dev/null +++ b/src/world/occupant-codes.pony @@ -0,0 +1,15 @@ +primitive OccupantCodes + fun none(): I32 => 0 + fun self(): I32 => 1 + fun raven(): I32 => 2 + fun goblin(): I32 => 3 + fun brigand(): I32 => 4 + fun ooze(): I32 => 5 + fun skeleton(): I32 => 6 + fun ekek(): I32 => 7 + fun hellhound(): I32 => 8 + fun cloaked_shadow(): I32 => 9 + fun mantis(): I32 => 10 + fun horror(): I32 => 11 + fun vampire(): I32 => 12 + \ No newline at end of file diff --git a/src/world/occupant.pony b/src/world/occupant.pony new file mode 100755 index 0000000..e04a471 --- /dev/null +++ b/src/world/occupant.pony @@ -0,0 +1,12 @@ +use "../agents" +use "../datast" +use "../display" + +interface Occupant + be update_pos(pos: Pos val) => None + be hit(hit_roll: I32, dmg: I32, attacker: Agent tag, attacker_name: String, + display: Display tag) => None + be describe(display: Display tag) + +actor EmptyOccupant is Occupant + be describe(display: Display tag) => None diff --git a/src/world/overworld.pony b/src/world/overworld.pony new file mode 100755 index 0000000..de62c5d --- /dev/null +++ b/src/world/overworld.pony @@ -0,0 +1,101 @@ +use "collections" +use "random" +use "time" +use "../agents" +use "../datast" +use "../display" +use "../game" +use "../generators" +use "../log" + +actor OverWorld is World + let _parent_world: World tag = EmptyWorld + let _diameter: I32 + let _diameter_per_region: I32 = 65 + let _tiles: Tiles + let _agents: Agents + let _display: Display tag + let _rand: Random + let _dice: Dice + let _turn_manager: TurnManager tag + let _depth: I32 = 0 + var _last_focus: Pos val = Pos(0, 0) + var _last_turn: I32 = 0 + + new create(diameter': I32, + t_manager: TurnManager tag, seed: U64, display': Display tag) => + _rand = MT(seed) + _dice = Dice(_rand) + _diameter = diameter' + _tiles = Tiles(_diameter, _diameter) + _turn_manager = t_manager + _display = display' + _agents = Agents(_display) + + try + let terrain = DiamondSquare(_diameter, _diameter_per_region, _rand) + for row in Range(0, _diameter.usize()) do + for col in Range(0, _diameter.usize()) do + let next_cell = Pos(row.i32(), col.i32()) + let elevation = terrain(next_cell) +// let elevation: ISize = 2 + match _dice(1, 3) + | 1 => _tiles(next_cell) = + (Tile(EmptyOccupant, OccupantCodes.none(), Plain, + elevation.isize())) + | 2 => _tiles(next_cell) = + (Tile(EmptyOccupant, OccupantCodes.none(), Forest, + elevation.isize())) + | 3 => _tiles(next_cell) = + (Tile(EmptyOccupant, OccupantCodes.none(), Hill, + elevation.isize())) + else + _tiles(next_cell) = (Tile(EmptyOccupant, OccupantCodes.none(), + Plain, elevation.isize())) + end + end + end + else + Logger.err("Can't generate terrain!") + end + + fun present(): Bool => true + + be increment_turn() => _last_turn = _last_turn + 1 + + be enter(self: Self) => + let initial_pos = Pos(_diameter / 2, _diameter / 2) + self.update_pos(initial_pos) + add_agent(self, initial_pos, OccupantCodes.self()) + set_occupant(initial_pos, self, OccupantCodes.self(), true) + self.update_world(this) + + fun ref _exit(pos: Pos val, self: Self) => + _last_focus = pos + remove_occupant(pos) + agents().remove(self) + + be add_agent(a: Agent tag, pos: Pos val, occupant_code: I32) => + _agents.add(a) + set_occupant(pos, a, occupant_code) + + fun display(): Display tag => _display + + fun diameter(): I32 => _diameter + + fun ref tile(pos: Pos val): Tile => + try + _tiles(pos) + else + Tile.empty() + end + + fun ref tiles(): Tiles => _tiles + + fun ref agents(): Agents => _agents + + fun depth(): I32 => _depth + + fun parent(): World tag => _parent_world + + fun turn_manager(): (TurnManager | None) => _turn_manager diff --git a/src/world/room.pony b/src/world/room.pony new file mode 100755 index 0000000..032471b --- /dev/null +++ b/src/world/room.pony @@ -0,0 +1,14 @@ +use "../datast" + +class Room + let _shape: RoomShape val + + new create(shape: RoomShape val) => + _shape = shape + + fun discover(t: Tiles) => + for pos in _shape.perimeter() do + t.discover(pos) + end + + fun rand_interior_position(): Pos val => _shape.rand_interior_position() diff --git a/src/world/simple-dungeon.pony b/src/world/simple-dungeon.pony new file mode 100755 index 0000000..b1175dc --- /dev/null +++ b/src/world/simple-dungeon.pony @@ -0,0 +1,83 @@ +use "collections" +use "random" +use "time" +use "../agents" +use "../datast" +use "../display" +use "../encounters" +use "../game" +use "../generators" +use "../guid" +use "../inventory" +use "../log" + +actor SimpleDungeon is World + let _parent_world: World tag + // TODO: Use determinate seed + let _guid_gen: GuidGenerator = GuidGenerator + let _display: Display tag + let _diameter: I32 + let _tiles: Tiles + let _agents: Agents + let _turn_manager: TurnManager tag + let _depth: I32 + var _last_focus: Pos val + var _present: Bool = false + var _last_turn: I32 = 0 + + new create(d: I32, t_manager: TurnManager tag, display': Display tag, + depth': I32 = 1, parent': World tag = EmptyWorld, + self: (Self | None) = None) + => + _diameter = d + _turn_manager = t_manager + _parent_world = parent' + _depth = depth' + let starting_pos = Pos(_diameter / 2, _diameter / 2) + _last_focus = starting_pos + _tiles = SimpleTiles(d) + _display = display' + _agents = Agents(_display) + + be increment_turn() => _last_turn = _last_turn + 1 + + be enter(self: Self) => + add_agent(self, _last_focus, OccupantCodes.self()) + self.update_world(this) + self.enter_world_at(_last_focus, _depth) + _display.log("Entering simple room") + _present = true + + fun ref _exit(pos: Pos val, self: Self) => + _last_focus = pos + remove_occupant(pos) + agents().remove(self) + _present = false + + be add_agent(a: Agent tag, pos: Pos val, occupant_code: I32) => + _agents.add(a) + set_occupant(pos, a, occupant_code) + + fun display(): Display tag => _display + + fun diameter(): I32 => _diameter + + fun ref tile(pos: Pos val): Tile => + try + _tiles(pos) + else + Tile.empty() + end + + fun ref tiles(): Tiles => _tiles + + fun ref agents(): Agents => _agents + + fun depth(): I32 => _depth + + fun parent(): World tag => _parent_world + + fun turn_manager(): (TurnManager | None) => _turn_manager + + fun present(): Bool => _present + diff --git a/src/world/terrain.pony b/src/world/terrain.pony new file mode 100755 index 0000000..00ed232 --- /dev/null +++ b/src/world/terrain.pony @@ -0,0 +1,40 @@ +trait Terrain + fun is_passable(): Bool => true + fun is_transparent(): Bool => true + fun is_discoverable(): Bool => false + fun description(): String + +primitive Plain is Terrain + fun description(): String => "empty plains" + +primitive Forest is Terrain + fun description(): String => "a forest" + +primitive Hill is Terrain + fun description(): String => "hills" + +primitive Floor is Terrain + fun description(): String => "the floor" + +primitive Wall is Terrain + fun is_passable(): Bool => false + fun is_transparent(): Bool => false + fun is_discoverable(): Bool => true + fun description(): String => "a wall" + +primitive Door is Terrain + fun is_transparent(): Bool => false + fun description(): String => "a door" + +primitive Lava is Terrain + fun description(): String => "some lava" + +primitive Undug is Terrain + fun is_passable(): Bool => false + fun is_transparent(): Bool => false + fun description(): String => ", well, you can't tell" + +primitive Void is Terrain + fun is_passable(): Bool => false + fun is_transparent(): Bool => false + fun description(): String => ", well, you can't tell" diff --git a/src/world/test.pony b/src/world/test.pony new file mode 100755 index 0000000..ebc7b7c --- /dev/null +++ b/src/world/test.pony @@ -0,0 +1,32 @@ +use "collections" +use "debug" +use "ponytest" +use "../datast" + +actor Main is TestList + new create(env: Env) => PonyTest(env, this) + + new make() => None + + fun tag tests(test: PonyTest) => + test(_TestLineIterator) + +class iso _TestLineIterator is UnitTest + fun name(): String => "line-of-sight:LineIterator" + + fun apply(h: TestHelper) => + var pos1 = Pos(0, 0) + var pos2 = Pos(5, 5) + var iter = LineIterator(pos1, pos2) + for pos in iter do + Debug(pos.string()) + end + + pos1 = Pos(0, 0) + pos2 = Pos(-5, -9) + iter = LineIterator(pos1, pos2) + for pos in iter do + Debug(pos.string()) + end + + // h.assert_eq[Pos val](p1.perimeter_space(i), perimeter_ans(i)) diff --git a/src/world/tile.pony b/src/world/tile.pony new file mode 100755 index 0000000..2f9ba03 --- /dev/null +++ b/src/world/tile.pony @@ -0,0 +1,228 @@ +use "../agents" +use "../datast" +use "../display" +use "../game" +use "../inventory" + +class Tile + var occupant: Occupant tag + var occupant_code: I32 + var item: (Item val | None) = None + var terrain: Terrain val + var landmark: Landmark val = EmptyLandmark + let elevation: ISize + let room_id: U128 + var lit: Bool + var _is_discovered: Bool + var _is_highlighted: Bool + var _is_seen: Bool + var _has_been_seen: Bool + + new create(o: Occupant tag, o_code: I32, t: Terrain val, + e: ISize = -1, i: (Item val | None) = None, + l: Landmark = EmptyLandmark, discovered: Bool = false, r_id: U128 = 0, + lit': Bool = true, h: Bool = false, s: Bool = false, has: Bool = false) + => + occupant = o + occupant_code = o_code + item = i + terrain = t + landmark = l + elevation = e + room_id = r_id + lit = lit' + _is_discovered = discovered + _is_highlighted = h + _is_seen = s + _has_been_seen = has + + new empty() => + occupant = EmptyOccupant + occupant_code = OccupantCodes.none() + terrain = Undug + elevation = -1 + room_id = 0 + lit = false + _is_discovered = false + _is_highlighted = false + _is_seen = false + _has_been_seen = false + + fun ref set_occupant(o: Occupant tag, o_code: I32, discovered: Bool = false) + => + occupant = o + occupant_code = o_code + if discovered then _is_discovered = true end + + fun ref remove_occupant() => + occupant = EmptyOccupant + occupant_code = OccupantCodes.none() + + fun ref set_item(i: Item val) => + item = i + + fun ref remove_item(): (Item val | None) => + item = None + + fun ref update_landmark(l: Landmark) => + landmark = l + + fun ref update_seen(s: Bool) => + if s == true then _has_been_seen = true end + _is_seen = s + + fun describe(display: Display tag) => + if (not is_self()) and is_occupied() then + occupant.describe(display) + else + match item + | let g: Gold val => display.log("You see some gold pieces.") + | let i: Item val => display.log("You see an item.") + else + if has_landmark() then + display.log("You see " + landmark.description()) + else + display.log("You see " + terrain.description()) + end + end + end + + fun describe_close(display: Display tag) => + match item + | let i: Item val => display.log("You see " + i.description()) + else + if has_landmark() then + display.log("You see " + landmark.description()) + else + display.log("You see " + terrain.description()) + end + end + + fun ref portal(diameter: I32, + turn_manager: TurnManager tag, display: Display tag, + depth: I32, parent: World tag): World tag + => + match landmark + | let d: DownStairs val => + if d.is_initialized() then + d.world + else + landmark = d.build_world(diameter, turn_manager, display, depth, + parent) + match landmark + | let ds: DownStairs val => + ds.world + else + EmptyWorld + end + end + else + EmptyWorld + end + + fun ref discover() => + if terrain.is_discoverable() then + _is_discovered = true + end + + fun ref highlight() => _is_highlighted = true + + fun ref unhighlight() => _is_highlighted = false + + fun is_discovered(): Bool => _is_discovered + + fun is_occupied(): Bool => + not (occupant_code == OccupantCodes.none()) + + fun is_interesting(): Bool => + (occupant_code != OccupantCodes.none()) or has_item() + or has_landmark() + + fun is_empty(): Bool => + (occupant_code == OccupantCodes.none()) and (not has_item()) + and (not has_landmark()) and is_passable() + + fun is_passable(): Bool => + (occupant_code == OccupantCodes.none()) and terrain.is_passable() + + fun is_visible(): Bool => is_discovered() or (_is_seen and lit) + + fun is_seen(): Bool => _is_seen + + fun is_transparent(): Bool => terrain.is_transparent() + + fun is_highlighted(): Bool => _is_highlighted + + fun is_diggable(): Bool => + match terrain + | Undug => true + else + false + end + + fun is_open(): Bool => + this.is_passable() and + (not this.is_occupied()) + + fun is_self(): Bool => + occupant_code == OccupantCodes.self() + + fun has_item(): Bool => + match item + | let i: Item val => true + else + false + end + + fun has_landmark(): Bool => + match landmark + | EmptyLandmark => false + else + true + end + + fun has_staircase(): Bool => + match landmark + | UpStairs => true + | let ds: DownStairs val => true + else + false + end + + fun has_upstairs(): Bool => + match landmark + | UpStairs => true + else + false + end + + fun has_downstairs(): Bool => + match landmark + | let ds: DownStairs val => true + else + false + end + + fun is_void(): Bool => + match terrain + | Void => true + else + false + end + + fun has_been_seen(): Bool => _has_been_seen + + fun clone(): Tile iso^ => + let o = occupant + let o_code = occupant_code + let t = terrain + let i = item + let l = landmark + let e = elevation + let d = _is_discovered + let r = room_id + let lit' = lit + let h = _is_highlighted + let s = _is_seen + let has = _has_been_seen + recover Tile(o, o_code, t, e, i, l, d, r, lit', h, s, has) end diff --git a/src/world/tiles.pony b/src/world/tiles.pony new file mode 100755 index 0000000..e49da9a --- /dev/null +++ b/src/world/tiles.pony @@ -0,0 +1,109 @@ +use "collections" +use "../datast" + +class Tiles + let _h: I32 + let _w: I32 + let _data: Array[Tile] + let _rooms: Map[U128, Room val] = Map[U128, Room val] + let _room_list: Array[Room val] = Array[Room val] + let _room_shapes: RangedArray[RoomShape val] = RangedArray[RoomShape val] + let _absolute_top_left: Pos val + + new create(h': I32, w': I32, data_gen: ({(): Tile} val | None) = None, + absolute_top_left': Pos val = Pos(0, 0)) + => + _h = h' + _w = w' + _absolute_top_left = absolute_top_left' + let cell_count = (_h * _w).usize() + _data = Array[Tile](cell_count) + match data_gen + | let f: {(): Tile} val => + for cell in Range(0, cell_count) do + _data.push(f()) + end + else + for cell in Range(0, cell_count) do + _data.push(Tile.empty()) + end + end + + new iso _from(h': I32, w': I32, d: Array[Tile] iso, + absolute_top_left': Pos val = Pos(0, 0)) + => + _h = h' + _w = w' + _data = consume d + _absolute_top_left = absolute_top_left' + + fun _to_cell(pos: Pos val): USize => + ((_w * pos.y) + pos.x).usize() + + fun _in_bounds(pos: Pos val): Bool => + (pos.y >= 0) and (pos.y < _h) and + (pos.x >= 0) and (pos.x < _w) + + fun ref apply(pos: Pos val): Tile ? => + if _in_bounds(pos) then + _data(_to_cell(pos)) + else + error + end + + fun h(): I32 => _h + fun w(): I32 => _w + + fun ref add_room(id: U128, shape: RoomShape val) => + let r: Room val = recover Room(shape) end + _rooms(id) = r + _room_list.push(r) + _room_shapes.add(shape, shape.perimeter_size()) + + // fun ref add_room_shape(room_shape: RoomShape val) => + // _room_shapes.add(room_shape, room_shape.perimeter_size()) + + fun ref discover_room(id: U128) => try _rooms(id).discover(this) end + + fun ref discover(pos: Pos val) => try apply(pos).discover() end + + fun ref update(pos: Pos val, value: Tile) ? => + if _in_bounds(pos) then + _data(_to_cell(pos)) = value + else + @printf[I32](("Matrix Write Error: Out of bounds\n").cstring()) + error + end + + fun ref submap(h': I32, w': I32, top_left_cell: Pos val, + create_void: ({(): Tile} val) = {(): Tile => Tile.empty()}): + Tiles iso^ + => + let data: Array[Tile] iso = recover Array[Tile] end + for row in Range(0, h'.usize()) do + for col in Range(0, w'.usize()) do + let x = col.i32() + top_left_cell.x + let y = row.i32() + top_left_cell.y + let tile = + try + apply(Pos(x, y)) + else + create_void() + end + data.push(tile.clone()) + end + end + Tiles._from(h', w', consume data, top_left_cell) + + fun relative_pos_for(abs: Pos val): Pos val => + abs - _absolute_top_left + + fun room_shapes(): this->RangedArray[RoomShape val] => _room_shapes + + fun room_count(): USize => _room_list.size() + + fun room(idx: USize): Room val ? => _room_list(idx) + + fun ref highlight(pos: Pos val) => try apply(pos).highlight() end + + fun ref unhighlight(pos: Pos val) => try apply(pos).unhighlight() end diff --git a/src/world/world-builder.pony b/src/world/world-builder.pony new file mode 100755 index 0000000..1c6a7e4 --- /dev/null +++ b/src/world/world-builder.pony @@ -0,0 +1,14 @@ +use "../agents" +use "../datast" +use "../display" +use "../game" + +interface WorldBuilder + fun apply(diameter: I32, turn_manager: TurnManager tag, display: Display tag, + depth: I32, parent: World tag): World tag + +primitive DungeonBuilder + fun apply(diameter: I32, turn_manager: TurnManager tag, display: Display tag, + depth: I32 = 1, parent: World tag = EmptyWorld): World tag + => + Dungeon(diameter, turn_manager, display, depth, parent) diff --git a/src/world/world.pony b/src/world/world.pony new file mode 100755 index 0000000..3e8b266 --- /dev/null +++ b/src/world/world.pony @@ -0,0 +1,238 @@ +use "collections" +use "random" +use "time" +use "../agents" +use "../datast" +use "../display" +use "../game" +use "../generators" +use "../inventory" + +trait World + fun ref tile(pos: Pos val): Tile + + fun ref is_valid_target(pos: Pos val): Bool => + let target: Tile = tile(pos) + target.is_passable() + + fun ref remove_occupant(pos: Pos val) => + tile(pos).remove_occupant() + + fun ref set_occupant(pos: Pos val, occupant: Occupant tag, + occupant_code: I32, discovered: Bool = false) => + tile(pos).set_occupant(occupant, occupant_code, discovered) + + be enter(self: Self) + + fun ref _exit(pos: Pos val, self: Self) + + be add_agent(a: Agent tag, pos: Pos val, occupant_code: I32) + + be add_agent_if_empty(a: Agent tag, pos: Pos val, occupant_code: I32) => + try + let t = tiles()(pos) + if t.is_open() then + add_agent(a, pos, occupant_code) + end + end + + be move_occupant(from: Pos val, to: Pos val, occupant: Occupant tag, + occupant_code: I32) => + if is_valid_target(to) then + remove_occupant(from) + set_occupant(to, occupant, occupant_code) + occupant.update_pos(to) + if occupant_code == OccupantCodes.self() then + match turn_manager() + | let tm: TurnManager tag => + tm.update_focus(to) + tm.ack() + end + let to_tile = tile(to) + discover_room(to_tile.room_id) + discover(to) + end + else + if occupant_code == OccupantCodes.self() then + match turn_manager() + | let tm: TurnManager tag => + tm.ack() + end + end + end + + be request_submap(radius: I32, agent: Agent tag, focus: Pos val) => + let diameter' = (radius * 2) + 1 + agent.deliver_submap(submap(diameter', focus), display()) + + be add_room(id: U128, shape: RoomShape val) => tiles().add_room(id, shape) + + be discover_room(id: U128) => + if id != 0 then + tiles().discover_room(id) + end + + fun diameter(): I32 + + fun ref discover(pos: Pos val) => tile(pos).discover() + + fun ref move_agents() => None + + fun ref tiles(): Tiles + + fun ref agents(): Agents + + be stop_agents() => agents().stop() + be restart_agents() => agents().restart() + + fun depth(): I32 + + fun parent(): World tag => EmptyWorld + + fun turn_manager(): (TurnManager | None) + + fun ref submap(diameter': I32, focus: Pos val): Tiles iso^ => + let top_left = Pos(focus.x - (diameter' / 2), focus.y - (diameter' / 2)) + tiles().submap(diameter', diameter', top_left) + + fun display(): Display tag + + be increment_turn() + + be display_map(h: I32, w: I32, focus: Pos val, d: Display tag) => + if present() then + let top_left = Pos(focus.x - (w / 2), focus.y - (h / 2)) + let sub = tiles().submap(h, w, top_left) + d(consume sub) + end + + be update_seen(h: I32, w: I32, focus: Pos val) => + let tlx = focus.x - ((w / 2) + 1) + let tly = focus.y - ((h / 2) + 1) + let top_left = Pos(tlx, tly) + let brx = focus.x + ((w / 2) + 1) + let bry = focus.y + ((h / 2) + 1) + let bottom_right = Pos(brx, bry) + let scan = Scan(top_left, h, w) + for pos in scan() do + try tiles()(pos).update_seen(false) end + end + let shape = RectRoom(top_left, bottom_right) + for pos in shape.perimeter() do + let line = LineIterator(focus, pos) + for p in line do + try + let t = tiles()(p) + if t.is_transparent() then + t.update_seen(true) + else + t.update_seen(true) + break + end + end + end + end + + be next_turn(t_manager: TurnManager tag, self_pos: Pos val) => + agents().prepare_act(t_manager, self_pos) + + be highlight(pos: Pos val) => + tiles().highlight(pos) + + be unhighlight(pos: Pos val) => + tiles().unhighlight(pos) + + be describe(pos: Pos val, d: Display tag) => + try + let t = tiles()(pos) + if t.is_visible() then + t.describe(d) + else + d.log("You can't see anything.") + end + end + + be describe_close(pos: Pos val, d: Display tag) => + try + let t = tiles()(pos) + if t.is_visible() and t.is_self() then + t.describe_close(d) + else + d.log("You can't see anything.") + end + end + + be try_take(pos: Pos val, s: Self tag) => + try + let t = tiles()(pos) + match t.item + | let i: Item val => + s.pick_up_item(i) + end + end + + be remove_item(pos: Pos val) => + try + let t = tiles()(pos) + t.remove_item() + end + + be try_add_item(i: Item val, pos: Pos val) => + try + let t = tiles()(pos) + match t.item + | let s: StaffOfEternity val => None + else + t.set_item(i) + end + end + + be climb(pos: Pos val, self: Self tag) => + try + let t = tiles()(pos) + if t.has_upstairs() then + _exit(pos, self) + parent().enter(self) + else + display().log("No stairs leading up!") + end + end + + be portal(pos: Pos val, self: Self tag) => + try + let t_manager = turn_manager() + match t_manager + | let tm: TurnManager tag => + let t = tiles()(pos) + if t.has_downstairs() then + _exit(pos, self) + let down = t.portal(diameter(), tm, display(), + depth() + 1, this) + down.enter(self) + else + display().log("No stairs leading down!") + end + end + end + + be process_death(a: Agent tag, pos: Pos val) => + remove_occupant(pos) + agents().remove(a) + a.confirm_death() + + fun present(): Bool + +actor EmptyWorld is World + be enter(self: Self) => None + fun ref _exit(pos: Pos val, self: Self) => None + fun diameter(): I32 => 0 + fun ref tile(pos: Pos val): Tile => Tile.empty() + fun ref is_valid_target(pos: Pos val): Bool => false + fun ref tiles(): Tiles => Tiles(0, 0) + be add_agent(a: Agent tag, pos: Pos val, occupant_code: I32) => None + fun ref agents(): Agents => Agents(EmptyDisplay) + fun turn_manager(): (TurnManager | None) => None + fun display(): Display tag => EmptyDisplay + fun depth(): I32 => 0 + be increment_turn() => None + fun present(): Bool => false