opal

OPAL's Playable Almost Indefectibly. OPAL is a rogue-like dungeon crawler.

Log | Files | << Repositories


tree a6352f4b4df3b3d9da6bee229c26244a08b89bb2
author esote <esote.net@gmail.com> 1555706127 -0500
committer esote <esote.net@gmail.com> 1555706127 -0500
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iHUEABYIAB0WIQTXAxYDuIzimYoNSPuhTmRAjzzC8gUCXSAqZwAKCRChTmRAjzzC
 8va/AQDPHHirzU+aMieP2h97hB5zM42W2B7ZTo2Lkgf+SXBy3gD9GQZsl0LdpRXW
 pG1EgmGHbdJ+cL+rsracipFnRJHslAU=
 =Bcsn
 -----END PGP SIGNATURE-----

OPAL

 .gitignore |   13 +
 LICENSE    |  662 ++++++++++++++++++++++++
 Makefile   |   47 ++
 README     |   10 +
 dijk.cpp   |  149 ++++++
 dijk.h     |   23 +
 floor.cpp  |  126 +++++
 floor.h    |   28 ++
 gen.cpp    |  425 ++++++++++++++++
 gen.h      |   35 ++
 globs.h    |  211 ++++++++
 opal.cpp   |  381 ++++++++++++++
 parse.cpp  |  129 +++++
 parse.h    |   24 +
 parse.l    |   97 ++++
 parse.y    |  288 +++++++++++
 rand.cpp   |   44 ++
 rand.h     |   54 ++
 turn.cpp   | 1649 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 turn.h     |   33 ++
 20 files changed, 4428 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c849820
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+*.gcda
+*.gcno
+*.gcov
+*.out
+*.swp
+*.tab.c
+*.tab.h
+*.txt
+error
+lex.yy.c
+vgcore.*
+y.dot
+y.output
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fe6b903
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,662 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  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.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "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. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  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 Affero General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e63c470
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+CXX = g++
+
+CFLAGS := -g
+CFLAGS += -std=c++17 -pedantic
+CFLAGS += -Wextra -Wall -Wshadow -Wconversion
+
+CFLAGS += -ftrapv -fverbose-asm
+CFLAGS += -Wundef -Wcast-align=strict -Wstrict-overflow=5
+CFLAGS += -Wwrite-strings -Wcast-qual -Wformat=2
+CFLAGS += -Wswitch-enum -Wunreachable-code
+CFLAGS += -pthread
+
+FAST_CFLAGS := -O3 -std=c++17 -pedantic -pthread
+
+BENCH_CFLAGS := -pg -fprofile-arcs -ftest-coverage -pthread
+
+CFLAGS_END := -lncurses
+
+DIRTY := *.gcda *.gcno *.gcov *.out error vgcore.*
+DIRTY += *.tab.c *.tab.h lex.yy.c y.dot y.output
+
+src := dijk.cpp floor.cpp gen.cpp rand.cpp opal.cpp parse.cpp turn.cpp
+hdr = dijk.h floor.h gen.h globs.h parse.h rand.h turn.h
+hdr += parse.l parse.y
+
+src_nodep := lex.yy.c y.tab.c
+
+opal: $(src) $(hdr)
+	lex --fast parse.l
+	yacc -d -l parse.y
+	$(CXX) $(FAST_CFLAGS) -o opal.out $(src) $(src_nodep) $(CFLAGS_END)
+
+debug: $(src) $(hdr)
+	lex -v -d parse.l
+	yacc -v -d parse.y
+	$(CXX) $(CFLAGS) -o opal.out $(src) $(src_nodep) $(CFLAGS_END)
+
+bench: $(src) $(hdr)
+	lex -d parse.l
+	yacc -d parse.y
+	$(CXX) $(BENCH_CFLAGS) -o opal.out $(src) $(src_nodep) $(CFLAGS_END)
+
+clean:
+	rm -f $(DIRTY)
+
+val:
+	valgrind -v --leak-check=full --show-leak-kinds=all --track-origins=yes --log-file=error ./opal.out -z 3656437442
diff --git a/README b/README
new file mode 100644
index 0000000..1bb01ea
--- /dev/null
+++ b/README
@@ -0,0 +1,10 @@
+OPAL's Playable Almost Indefectibly. OPAL is a rogue-like dungeon crawler. Get
+flag info using ./opal.out -h.
+
+Damage is calculated as the sum of dice rolls for the player's base damage and
+all equipped items.
+
+The monster and object parsing is implemented in Yacc and (F)Lex, so compilation
+requires them.
+
+Requires support for C++17 and POSIX threads.
diff --git a/dijk.cpp b/dijk.cpp
new file mode 100644
index 0000000..9c5e357
--- /dev/null
+++ b/dijk.cpp
@@ -0,0 +1,149 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <algorithm>
+#include <functional>
+#include <thread>
+#include "globs.h"
+
+static void	dijkstra_d();
+static void	dijkstra_dt();
+
+static void constexpr	calc_cost_d(tile const &, tile &);
+static void constexpr	calc_cost_dt(tile const &, tile &);
+
+struct compare_d {
+	bool constexpr
+	operator() (tile const &a, tile const &b) const
+	{
+		return a.d > b.d;
+	}
+};
+
+struct compare_dt {
+	bool constexpr
+	operator() (tile const &a, tile const &b) const
+	{
+		return a.dt > b.dt;
+	}
+};
+
+void
+dijkstra()
+{
+	std::thread t1(dijkstra_d);
+	std::thread t2(dijkstra_dt);
+
+	t1.join();
+	t2.join();
+}
+
+static void
+dijkstra_d()
+{
+	std::vector<std::reference_wrapper<tile>> heap;
+
+	for (std::size_t i = 1; i < HEIGHT - 1; ++i) {
+		for (std::size_t j = 1; j < WIDTH - 1; ++j) {
+			tiles[i][j].d = std::numeric_limits<int32_t>::max();
+
+			if (tiles[i][j].h == 0) {
+				tiles[i][j].vd = true;
+				heap.push_back(tiles[i][j]);
+			}
+		}
+	}
+
+	tiles[player.y][player.x].d = 0;
+
+	while (!heap.empty()) {
+		std::make_heap(heap.begin(), heap.end(), compare_d());
+
+		tile &t = heap.front();
+		std::pop_heap(heap.begin(), heap.end(), compare_d());
+		heap.pop_back();
+
+		calc_cost_d(t, tiles[t.y - 1][t.x + 0]);
+		calc_cost_d(t, tiles[t.y + 1][t.x + 0]);
+
+		calc_cost_d(t, tiles[t.y + 0][t.x - 1]);
+		calc_cost_d(t, tiles[t.y + 0][t.x + 1]);
+
+		calc_cost_d(t, tiles[t.y + 1][t.x + 1]);
+		calc_cost_d(t, tiles[t.y - 1][t.x - 1]);
+
+		calc_cost_d(t, tiles[t.y - 1][t.x + 1]);
+		calc_cost_d(t, tiles[t.y + 1][t.x - 1]);
+
+		t.vd = false;
+	}
+}
+
+static void
+dijkstra_dt()
+{
+	std::vector<std::reference_wrapper<tile>> heap;
+	heap.reserve((HEIGHT - 1) * (WIDTH - 1));
+
+	for (std::size_t i = 1; i < HEIGHT - 1; ++i) {
+		for (std::size_t j = 1; j < WIDTH - 1; ++j) {
+			tiles[i][j].dt = std::numeric_limits<int32_t>::max();
+			tiles[i][j].vdt = true;
+			heap.push_back(tiles[i][j]);
+		}
+	}
+
+	tiles[player.y][player.x].dt = 0;
+
+	while (!heap.empty()) {
+		std::make_heap(heap.begin(), heap.end(), compare_dt());
+
+		tile &t = heap.front();
+		std::pop_heap(heap.begin(), heap.end(), compare_dt());
+		heap.pop_back();
+
+		calc_cost_dt(t, tiles[t.y - 1][t.x + 0]);
+		calc_cost_dt(t, tiles[t.y + 1][t.x + 0]);
+
+		calc_cost_dt(t, tiles[t.y + 0][t.x - 1]);
+		calc_cost_dt(t, tiles[t.y + 0][t.x + 1]);
+
+		calc_cost_dt(t, tiles[t.y + 1][t.x + 1]);
+		calc_cost_dt(t, tiles[t.y - 1][t.x - 1]);
+
+		calc_cost_dt(t, tiles[t.y - 1][t.x + 1]);
+		calc_cost_dt(t, tiles[t.y + 1][t.x - 1]);
+
+		t.vdt = false;
+	}
+}
+
+static void constexpr
+calc_cost_d(tile const &a, tile &b)
+{
+	if (b.vd && b.d > a.d) {
+		b.d = a.d + 1;
+	}
+}
+
+static void constexpr
+calc_cost_dt(tile const &a, tile &b)
+{
+	if (b.vdt && b.dt > a.dt + a.h/TUNNEL_STRENGTH) {
+		b.dt = a.dt + 1 + a.h/TUNNEL_STRENGTH;
+	}
+}
diff --git a/dijk.h b/dijk.h
new file mode 100644
index 0000000..30a6dca
--- /dev/null
+++ b/dijk.h
@@ -0,0 +1,23 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef DIJK_H
+#define DIJK_H
+
+void	dijkstra();
+
+#endif /* DIJK_H */
diff --git a/floor.cpp b/floor.cpp
new file mode 100644
index 0000000..d45c64c
--- /dev/null
+++ b/floor.cpp
@@ -0,0 +1,126 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "globs.h"
+
+static bool	valid_room(room const &);
+static int	valid_corridor_x(int const, int const);
+static int	valid_corridor_y(int const, int const);
+static int	valid_stair(int const, int const);
+
+static int constexpr MINROOMH = 4;
+static int constexpr MINROOMW = 3;
+static int constexpr MAXROOMH = 8;
+static int constexpr MAXROOMW = 15;
+
+bool
+gen_room(room &r)
+{
+	r.x = rr.rrand<uint8_t>(1, WIDTH - 2);
+	r.y = rr.rrand<uint8_t>(1, HEIGHT - 2);
+	r.size_x = rr.rrand<uint8_t>(MINROOMW, MAXROOMW);
+	r.size_y = rr.rrand<uint8_t>(MINROOMH, MAXROOMH);
+
+	return valid_room(r);
+}
+
+void
+draw_room(room const &r)
+{
+	for (int i = r.x; i < r.x + r.size_x; ++i) {
+		for (int j = r.y; j < r.y + r.size_y; ++j) {
+			tiles[j][i].c = ROOM;
+			tiles[j][i].h = 0;
+		}
+	}
+}
+
+
+void
+gen_corridor(room const &r1, room const &r2)
+{
+	for (int i = std::min(r1.x, r2.x); i <= std::max(r1.x, r2.x); ++i) {
+		if (valid_corridor_y(r1.y, i)) {
+			tiles[r1.y][i].c = CORRIDOR;
+			tiles[r1.y][i].h = 0;
+		}
+	}
+
+	for (int i = std::min(r1.y, r2.y); i <= std::max(r1.y, r2.y); ++i) {
+		if (valid_corridor_x(i, r2.x)) {
+			tiles[i][r2.x].c = CORRIDOR;
+			tiles[i][r2.x].h = 0;
+		}
+	}
+}
+
+void
+gen_stair(stair &s, bool const up)
+{
+	uint8_t x, y;
+
+	do {
+		x = rr.rrand<uint8_t>(1, WIDTH - 2);
+		y = rr.rrand<uint8_t>(1, HEIGHT - 2);
+	} while (!valid_stair(y, x));
+
+	tiles[y][x].c = up ? STAIR_UP : STAIR_DN;
+	tiles[y][x].h = 0;
+
+	s.x = x;
+	s.y = y;
+}
+
+static bool
+valid_room(room const &r)
+{
+	for (int i = r.x - 1; i <= r.x + r.size_x + 1; ++i) {
+		for (int j = r.y - 1; j <= r.y + r.size_y + 1; ++j) {
+			if (tiles[j][i].c != ROCK) {
+				return false;
+			}
+		}
+	}
+
+	return true;
+}
+
+static int
+valid_corridor_x(int const y, int const x)
+{
+	return tiles[y][x].c == ROCK
+		&& tiles[y][x + 1].c != CORRIDOR
+		&& tiles[y][x - 1].c != CORRIDOR;
+}
+
+static int
+valid_corridor_y(int const y, int const x)
+{
+	return tiles[y][x].c == ROCK
+		&& tiles[y + 1][x].c != CORRIDOR
+		&& tiles[y - 1][x].c != CORRIDOR;
+}
+
+static int
+valid_stair(int const y, int const x)
+{
+	return (tiles[y][x].c == ROCK || tiles[y][x].c == ROOM)
+		&& (tiles[y + 1][x].c == CORRIDOR
+		|| tiles[y - 1][x].c == CORRIDOR
+		|| tiles[y][x + 1].c == CORRIDOR
+		|| tiles[y][x - 1].c == CORRIDOR);
+}
diff --git a/floor.h b/floor.h
new file mode 100644
index 0000000..f6e26ac
--- /dev/null
+++ b/floor.h
@@ -0,0 +1,28 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef ROOM_H
+#define ROOM_H
+
+#include "globs.h"
+
+bool	gen_room(room &);
+void	draw_room(room const &);
+void	gen_corridor(room const &, room const &);
+void	gen_stair(stair &, bool const);
+
+#endif /* ROOM_H */
diff --git a/gen.cpp b/gen.cpp
new file mode 100644
index 0000000..291dabe
--- /dev/null
+++ b/gen.cpp
@@ -0,0 +1,425 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#define _DEFAULT_SOURCE
+#define _BSD_SOURCE
+#include <endian.h>
+#undef _BSD_SOURCE
+#undef _DEFAULT_SOURCE
+
+#include <sys/stat.h>
+
+#include <err.h>
+
+#include "floor.h"
+#include "globs.h"
+
+static bool	save_things(FILE *const);
+static bool	load_things(FILE *const);
+
+static void	init_fresh();
+static void	place_player();
+static int	valid_player(int const, int const);
+
+static char const *const DIRECTORY = "/.rlg327";
+static char const *const FILEPATH = "/dungeon";
+static int constexpr DF_L = 8;
+
+static char const *const MARK = "RLG327-S2019";
+static int constexpr MARK_L = 12;
+
+static int constexpr NEW_ROOM_COUNT = 8;
+static int constexpr ROOM_RETRIES = 150;
+
+static std::vector<stair> stairs_up;
+static std::vector<stair> stairs_dn;
+
+static uint16_t room_count;
+static std::vector<room> rooms;
+
+static uint16_t stair_up_count;
+static uint16_t stair_dn_count;
+
+tile tiles[HEIGHT][WIDTH];
+
+std::string
+rlg_path()
+{
+	char *home;
+#ifdef _GNU_SOURCE
+	home = secure_getenv("HOME");
+#else
+	home = getenv("HOME");
+#endif
+
+	if (home == NULL) {
+#ifdef _GNU_SOURCE
+		errx(1, "getenv");
+#else
+		errx(1, "secure_getenv");
+#endif
+	}
+
+	return std::string(home) + DIRECTORY;
+}
+
+bool
+save_dungeon()
+{
+	struct stat st;
+	FILE *f;
+	bool ret;
+
+	std::string path = rlg_path();
+
+	if (stat(path.c_str(), &st) == -1) {
+		if (errno == ENOENT) {
+			if (mkdir(path.c_str(), 0700) == -1) {
+				err(1, "save mkdir");
+			}
+		} else {
+			err(1, "save stat");
+		}
+	}
+
+	path += FILEPATH;
+
+	if (!(f = fopen(path.c_str(), "w"))) {
+		err(1, "save fopen");
+	}
+
+	ret = save_things(f);
+
+	if (fclose(f) == EOF) {
+		err(1, "save fclose, (save_things=%d)", ret);
+	}
+
+	return ret;
+}
+
+bool
+load_dungeon()
+{
+	FILE *f;
+	bool ret;
+	std::string path = rlg_path() + FILEPATH;
+
+	if (!(f = fopen(path.c_str(), "r"))) {
+		err(1, "load fopen");
+	}
+
+	ret = load_things(f);
+
+	if (fclose(f) == EOF) {
+		err(1, "load fclose, (load_things=%d)", ret);
+	}
+
+	return ret;
+}
+
+void
+clear_tiles()
+{
+	for (uint8_t i = 0; i < HEIGHT; ++i) {
+		for (uint8_t j = 0; j < WIDTH; ++j) {
+			tiles[i][j] = {};
+			tiles[i][j].x = j;
+			tiles[i][j].y = i;
+
+			if (i == 0 || j == 0 || i == HEIGHT - 1
+				|| j == WIDTH - 1) {
+				tiles[i][j].h = std::numeric_limits<uint8_t>::max();
+				tiles[i][j].d = std::numeric_limits<int32_t>::max();
+				tiles[i][j].dt = std::numeric_limits<int32_t>::max();
+			} else {
+				tiles[i][j].c = ROCK;
+				tiles[i][j].h = rr.rrand<uint8_t>(1,
+					std::numeric_limits<uint8_t>::max() - 1);
+			}
+		}
+	}
+}
+
+void
+arrange_new()
+{
+	room_count = NEW_ROOM_COUNT;
+	stair_up_count = rr.rrand<uint16_t>(1, (uint16_t)((room_count / 4) + 1));
+	stair_dn_count = rr.rrand<uint16_t>(1, (uint16_t)((room_count / 4) + 1));
+
+	rooms.resize(room_count);
+	stairs_up.resize(stair_up_count);
+	stairs_dn.resize(stair_dn_count);
+
+	init_fresh();
+}
+
+void
+arrange_loaded()
+{
+	for (auto const &r : rooms) {
+		draw_room(r);
+	}
+
+	for (auto const &s : stairs_up) {
+		tiles[s.y][s.x].c = STAIR_UP;
+	}
+
+	for (auto const &s : stairs_dn) {
+		tiles[s.y][s.x].c = STAIR_DN;
+	}
+
+	for (int i = 1; i < HEIGHT - 1; ++i) {
+		for (int j = 1; j < WIDTH - 1; ++j) {
+			if (tiles[i][j].h == 0 && tiles[i][j].c == ROCK) {
+				tiles[i][j].c = CORRIDOR;
+			}
+		}
+	}
+}
+
+void
+arrange_renew()
+{
+	clear_tiles();
+
+	rooms.clear();
+	stairs_up.clear();
+	stairs_dn.clear();
+
+	arrange_new();
+}
+
+static bool
+save_things(FILE *const f)
+{
+	uint32_t const ver = htobe32(0);
+	uint32_t const filesize = htobe32((uint32_t)(1708 + (room_count * 4)
+		+ (stair_up_count * 2) + (stair_dn_count * 2)));
+
+	/* type marker */
+	if (fwrite(MARK, MARK_L, 1, f) != 1) {
+		return false;
+	}
+
+	/* version */
+	if (fwrite(&ver, sizeof(uint32_t), 1, f) != 1) {
+		return false;
+	}
+
+	/* size */
+	if (fwrite(&filesize, sizeof(uint32_t), 1, f) != 1) {
+		return false;
+	}
+
+	/* player coords */
+	if (fwrite(&player.x, sizeof(uint8_t), 1, f) != 1) {
+		return false;
+	}
+	if (fwrite(&player.y, sizeof(uint8_t), 1, f) != 1) {
+		return false;
+	}
+
+	/* hardness */
+	for (std::size_t i = 0; i < HEIGHT; ++i) {
+		for (std::size_t j = 0; j < WIDTH; ++j) {
+			if (fwrite(&tiles[i][j].h, sizeof(uint8_t), 1, f) != 1) {
+				return false;
+			}
+		}
+	}
+
+	/* room num */
+	room_count = htobe16(room_count);
+	if (fwrite(&room_count, sizeof(uint16_t), 1, f) != 1) {
+		return false;
+	}
+	room_count = be16toh(room_count);
+
+	/* room data */
+	for (auto const &r : rooms) {
+		if (fwrite(&r, sizeof(room), 1, f) != 1) {
+			return false;
+		}
+	}
+
+	/* stairs_up num */
+	stair_up_count = htobe16(stair_up_count);
+	if (fwrite(&stair_up_count, sizeof(uint16_t), 1, f) != 1) {
+		return false;
+	}
+	stair_up_count = be16toh(stair_up_count);
+
+	/* stars_up coords */
+	for (auto const &s : stairs_up) {
+		if (fwrite(&s, sizeof(stair), 1, f) != 1) {
+			return false;
+		}
+	}
+
+	/* stairs_dn num */
+	stair_dn_count = htobe16(stair_dn_count);
+	if (fwrite(&stair_dn_count, sizeof(uint16_t), 1, f) != 1) {
+		return false;
+	}
+	stair_dn_count = be16toh(stair_dn_count);
+
+	/* stairs_dn coords */
+	for (auto const &s : stairs_dn) {
+		if (fwrite(&s, sizeof(stair), 1, f) != 1) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static bool
+load_things(FILE *const f)
+{
+	/* skip type marker, version, and size */
+	if (fseek(f, MARK_L + 2 * sizeof(uint32_t), SEEK_SET) == -1) {
+		return false;
+	}
+
+	/* player coords */
+	if (fread(&player.x, sizeof(uint8_t), 1, f) != 1) {
+		return false;
+	}
+	if (fread(&player.y, sizeof(uint8_t), 1, f) != 1) {
+		return false;
+	}
+
+	/* hardness */
+	for (std::size_t i = 0; i < HEIGHT; ++i) {
+		for (std::size_t j = 0; j < WIDTH; ++j) {
+			if (fread(&tiles[i][j].h, sizeof(uint8_t), 1, f) != 1) {
+				return false;
+			}
+		}
+	}
+
+	/* room num */
+	if (fread(&room_count, sizeof(uint16_t), 1, f) != 1) {
+		return false;
+	}
+	room_count = be16toh(room_count);
+
+	rooms.resize(room_count);
+
+	/* room data */
+	for (auto &r : rooms) {
+		if (fread(&r, sizeof(room), 1, f) != 1) {
+			return false;
+		}
+	}
+
+	/* stair_up num */
+	if (fread(&stair_up_count, sizeof(uint16_t), 1, f) != 1) {
+		return false;
+	}
+	stair_up_count = be16toh(stair_up_count);
+
+	stairs_up.resize(stair_up_count);
+
+	/* stair_up coords */
+	for (auto &s : stairs_up) {
+		if (fread(&s, sizeof(stair), 1, f) != 1) {
+			return false;
+		}
+	}
+
+	/* stair_dn num */
+	if (fread(&stair_dn_count, sizeof(uint16_t), 1, f) != 1) {
+		return false;
+	}
+	stair_dn_count = be16toh(stair_dn_count);
+
+	stairs_dn.resize(stair_dn_count);
+
+	/* stair_dn_coords */
+	for (auto &s : stairs_dn) {
+		if (fread(&s, sizeof(stair), 1, f) != 1) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static void
+init_fresh()
+{
+	std::size_t i = 0;
+	std::size_t retries = 0;
+
+	for (auto it = rooms.begin(); it != rooms.end()
+		&& retries < ROOM_RETRIES; ++it) {
+		if (!gen_room(*it)) {
+			retries++;
+			it--;
+		} else {
+			i++;
+			draw_room(*it);
+		}
+	}
+
+	if (i < room_count) {
+		if (i == 0) {
+			errx(1, "unable to place any rooms");
+		}
+
+		room_count = (uint16_t)i;
+		rooms.resize(room_count);
+	}
+
+	for (i = 0; i < room_count - 1U; ++i) {
+		gen_corridor(rooms[i], rooms[i+1]);
+	}
+
+	for (auto &s : stairs_up) {
+		gen_stair(s, true);
+	}
+
+	for (auto &s : stairs_dn) {
+		gen_stair(s, true);
+	}
+
+	place_player();
+}
+
+static void
+place_player()
+{
+	uint8_t x, y;
+
+	do {
+		x = rr.rrand<uint8_t>(1, WIDTH - 2);
+		y = rr.rrand<uint8_t>(1, HEIGHT - 2);
+	} while (!valid_player(y, x));
+
+	player.x = x;
+	player.y = y;
+}
+
+static int
+valid_player(int const y, int const x)
+{
+	return tiles[y][x].h == 0
+		&& tiles[y + 1][x].h == 0 && tiles[y - 1][x].h == 0
+		&& tiles[y][x + 1].h == 0 && tiles[y][x - 1].h == 0;
+}
diff --git a/gen.h b/gen.h
new file mode 100644
index 0000000..1bea4d9
--- /dev/null
+++ b/gen.h
@@ -0,0 +1,35 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef GEN_H
+#define GEN_H
+
+#include <string>
+
+std::string	rlg_path();
+
+/* io */
+bool	save_dungeon();
+bool	load_dungeon();
+
+/* gen */
+void	clear_tiles();
+void	arrange_new();
+void	arrange_loaded();
+void	arrange_renew();
+
+#endif /* GEN_H */
diff --git a/globs.h b/globs.h
new file mode 100644
index 0000000..6749c1b
--- /dev/null
+++ b/globs.h
@@ -0,0 +1,211 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef GLOBS_H
+#define GLOBS_H
+
+#include <cstdint>
+#include <ncurses.h>
+#include <vector>
+
+#include "rand.h"
+
+int constexpr WIDTH = 80;
+int constexpr HEIGHT = 21;
+
+int constexpr TUNNEL_STRENGTH = 85;
+
+char constexpr PLAYER = '@';
+char constexpr ROOM = '.';
+char constexpr CORRIDOR = '#';
+char constexpr ROCK = ' ';
+char constexpr STAIR_UP = '<';
+char constexpr STAIR_DN = '>';
+
+enum color {
+	black,
+	blue,
+	cyan,
+	green,
+	magenta,
+	red,
+	white,
+	yellow
+};
+
+uint16_t constexpr SMART = 1 << 0;
+uint16_t constexpr TELE = 1 << 1;
+uint16_t constexpr TUNNEL = 1 << 2;
+uint16_t constexpr ERRATIC = 1 << 3;
+
+uint16_t constexpr PLAYER_TYPE = 1 << 9;
+
+uint16_t constexpr BOSS = 1 << 4;
+uint16_t constexpr DESTROY = 1 << 5;
+uint16_t constexpr PASS = 1 << 6;
+uint16_t constexpr PICKUP = 1 << 7;
+uint16_t constexpr UNIQ = 1 << 8;
+
+struct dice {
+	uint64_t	base;
+	uint64_t	dice;
+	uint64_t	sides;
+};
+
+struct dungeon_thing {
+	std::string	desc;
+	std::string	name;
+	uint64_t	speed;
+	dice		dam;
+
+	/* first color as ncurses COLOR_PAIR(COLOR_*) value */
+	int		color;
+
+	unsigned int	symb;
+	uint8_t		rrty;
+	uint8_t		x;
+	uint8_t		y;
+	bool		done;
+
+	dungeon_thing() = default;
+
+	dungeon_thing(dungeon_thing const &t)
+	{
+		color = t.color;
+		desc = t.desc;
+		name = t.name;
+		dam = t.dam;
+		symb = t.symb;
+		rrty = t.rrty;
+		speed = t.speed;
+		x = 0;
+		y = 0;
+		done = false;
+	}
+};
+
+struct npc : dungeon_thing {
+	uint64_t	hp;
+	uint64_t	p_count;
+	uint64_t	turn;
+	uint16_t	type;
+	bool		dead;
+
+	npc() = default;
+
+	npc(npc const &n) : dungeon_thing(n)
+	{
+		type = n.type;
+		hp = n.hp;
+		turn = 0;
+		p_count = 0;
+		dead = n.dead;
+	}
+};
+
+enum type {
+	ammunition,
+	amulet,
+	armor,
+	book,
+	boots,
+	cloak,
+	container,
+	flask,
+	food,
+	gloves,
+	gold,
+	helmet,
+	light,
+	offhand,
+	ranged,
+	ring,
+	scroll_type, /* avoid conflict with ncurses */
+	wand,
+	weapon
+};
+
+struct obj : dungeon_thing {
+	uint64_t	def;
+	uint64_t	dodge;
+	uint64_t	hit;
+	uint64_t	val;
+	uint64_t	weight;
+	uint64_t	attr;
+	type		obj_type;
+	bool		art;
+
+	obj() = default;
+
+	obj(obj const &o) : dungeon_thing(o)
+	{
+		def = o.def;
+		dodge = o.dodge;
+		hit = o.hit;
+		val = o.val;
+		weight = o.weight;
+		attr = o.attr;
+		obj_type = o.obj_type;
+		art = o.art;
+	}
+};
+
+struct room {
+	uint8_t	x;
+	uint8_t	y;
+	uint8_t	size_x;
+	uint8_t	size_y;
+};
+
+struct stair {
+	uint8_t	x;
+	uint8_t	y;
+};
+
+struct tile {
+	/* turn engine */
+	npc	*n;
+	obj	*o;
+
+	uint8_t	h; /* hardness */
+	chtype	c; /* character */
+
+	uint8_t	x;
+	uint8_t	y;
+
+	/* dijkstra distance cost */
+	int32_t	d;
+	int32_t	dt;
+
+	/* dijkstra valid node */
+	bool	vd;
+	bool	vdt;
+
+	/* visited by PC */
+	bool	v;
+};
+
+extern ranged_random rr;
+
+extern npc player;
+
+extern tile tiles[HEIGHT][WIDTH];
+
+extern std::vector<npc> npcs_parsed;
+extern std::vector<obj> objs_parsed;
+
+#endif /* GLOBS_H */
diff --git a/opal.cpp b/opal.cpp
new file mode 100644
index 0000000..5f21216
--- /dev/null
+++ b/opal.cpp
@@ -0,0 +1,381 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <algorithm>
+#include <chrono>
+#include <iostream>
+#include <limits>
+#include <thread>
+
+#include <err.h>
+#include <getopt.h>
+
+#include "gen.h"
+#include "globs.h"
+#include "parse.h"
+#include "turn.h"
+
+static void	usage(int const, std::string const &);
+
+static bool	colors();
+
+static void	print_deathscreen(WINDOW *const);
+static void	print_winscreen1(WINDOW *const);
+static void	print_winscreen2(WINDOW *const);
+
+static bool	is_number(std::string const &);
+
+static char const *const PROGRAM_NAME = "opal";
+
+static struct option const long_opts[] = {
+	{"nodescs", no_argument, NULL, 'd'},
+	{"help", no_argument, NULL, 'h'},
+	{"load", no_argument, NULL, 'l'},
+	{"numnpcs", required_argument, NULL, 'n'},
+	{"numobjs", required_argument, NULL, 'o'},
+	{"save", no_argument, NULL, 's'},
+	{"seed", required_argument, NULL, 'z'},
+	{NULL, 0, NULL, 0}
+};
+
+npc player;
+
+int
+main(int const argc, char *const argv[])
+{
+	WINDOW *win;
+	char *end;
+	int ch;
+	bool load = false;
+	bool save = false;
+	bool no_descs = false;
+	unsigned int numnpcs = std::numeric_limits<unsigned int>::max();
+	unsigned int numobjs = std::numeric_limits<unsigned int>::max();
+	std::string const name = (argc == 0) ? PROGRAM_NAME : argv[0];
+
+	while ((ch = getopt_long(argc, argv, "dhln:o:sz:", long_opts, NULL)) != -1) {
+		switch(ch) {
+		case 'd':
+			no_descs = true;
+			break;
+		case 'h':
+			usage(EXIT_SUCCESS, name);
+			break;
+		case 'l':
+			load = true;
+			break;
+		case 'n':
+			numnpcs = (unsigned int)strtoul(optarg, &end, 10);
+
+			if (optarg == end || errno == EINVAL || errno == ERANGE) {
+				err(1, "numnpcs invalid");
+			}
+			break;
+		case 'o':
+			numobjs = (unsigned int)strtoul(optarg, &end, 10);
+
+			if (optarg == end || errno == EINVAL || errno == ERANGE) {
+				err(1, "numobjs invalid");
+			}
+			break;
+		case 's':
+			save = true;
+			break;
+		case 'z':
+			if (is_number(optarg)) {
+				rr = ranged_random(strtoul(optarg, &end, 10));
+
+				if (optarg == end || errno == EINVAL
+					|| errno == ERANGE) {
+					err(1, "seed %s invalid", optarg);
+				}
+			} else {
+				rr = ranged_random(optarg);
+			}
+			break;
+		default:
+			usage(EXIT_FAILURE, name);
+		}
+	}
+
+	if (numnpcs == std::numeric_limits<unsigned int>::max()) {
+		numnpcs = rr.rrand<unsigned int>(3, 5);
+	}
+
+	if (numobjs == std::numeric_limits<unsigned int>::max()) {
+		numobjs = rr.rrand<unsigned int>(10, 15);
+	}
+
+	(void)initscr();
+
+	if (!colors()) {
+		errx(1, "color init");
+	}
+
+	/* requires colors initialized */
+	if (!no_descs) {
+		parse_npc_file();
+		parse_obj_file();
+	}
+
+	if (refresh() == ERR) {
+		errx(1, "refresh from initscr");
+	}
+
+	if ((win = newwin(HEIGHT, WIDTH, 0, 0)) == NULL) {
+		errx(1, "newwin");
+	}
+
+	(void)box(win, 0, 0);
+
+	if (curs_set(0) == ERR) {
+		errx(1, "curs_set");
+	}
+
+	if (noecho() == ERR) {
+		errx(1, "noecho");
+	}
+
+	if (raw() == ERR) {
+		errx(1, "raw");
+	}
+
+	if (keypad(win, true) == ERR) {
+		errx(1, "keypad");
+	}
+
+	clear_tiles();
+
+	if (load) {
+		if (!load_dungeon()) {
+			errx(1, "loading dungeon");
+		}
+
+		arrange_loaded();
+	} else {
+		arrange_new();
+	}
+
+	player.color = COLOR_PAIR(COLOR_YELLOW);
+	player.dam = {0, 1, 4};
+	player.hp = rr.rand_dice<uint64_t>(50, 2, 50);
+	player.speed = 10;
+	player.symb = PLAYER;
+	player.turn = 0;
+	player.type = PLAYER_TYPE;
+
+	retry:
+	switch(turn_engine(win, numnpcs, numobjs)) {
+	case TURN_DEATH:
+		std::this_thread::sleep_for(std::chrono::seconds(1));
+		print_deathscreen(win);
+		std::this_thread::sleep_for(std::chrono::seconds(1));
+		break;
+	case TURN_NEXT:
+		if (werase(win) == ERR) {
+			errx(1, "arrange_renew erase");
+		}
+
+		(void)box(win, 0, 0);
+
+		arrange_renew();
+
+		for (auto &n : npcs_parsed) {
+			if (n.type & BOSS) {
+				n.done = false;
+			}
+		}
+
+		goto retry;
+	case TURN_NONE:
+		errx(1, "turn_engine return value invalid");
+		break;
+	case TURN_QUIT:
+		break;
+	case TURN_WIN:
+		std::this_thread::sleep_for(std::chrono::seconds(1));
+		if (rr.rrand<int>(0, 1) == 0) {
+			print_winscreen1(win);
+		} else {
+			print_winscreen2(win);
+		}
+		std::this_thread::sleep_for(std::chrono::seconds(1));
+		break;
+	}
+
+	if (delwin(win) == ERR) {
+		errx(1, "delwin");
+	}
+
+	if (endwin() == ERR) {
+		errx(1, "endwin");
+	}
+
+	std::cout << "seed: " << rr.seed << '\n';
+
+	if (save && !save_dungeon()) {
+		errx(1, "saving dungeon");
+	}
+
+	return EXIT_SUCCESS;
+}
+
+static void
+usage(int const status, std::string const &name)
+{
+	std::cout << "Usage: " << name << " [OPTION]... \n\n";
+
+	if (status != EXIT_SUCCESS) {
+		std::cerr << "Try '" << name <<
+			" --help' for more information.\n";
+	} else {
+		std::cout << "OPAL's Playable Almost Indefectibly.\n\n"
+			<< "Traverse a generated dungeon.\n\n"
+			<< "Options:\n\
+  -d, --nodescs         don't parse description files\n\
+  -h, --help            display this help text and exit\n\
+  -l, --load            load dungeon file\n\
+  -n, --numnpcs=[NUM]   number of npcs per floor\n\
+  -o, --numobjs=[NUM]   number of objs per floor\n\
+  -s, --save            save dungeon file\n\
+  -z, --seed=[SEED]     set rand seed, takes integer or string\n";
+	}
+
+	exit(status);
+}
+
+static bool
+colors()
+{
+	if (start_color() == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_BLUE, COLOR_BLUE, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_CYAN, COLOR_CYAN, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_GREEN, COLOR_GREEN, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_RED, COLOR_RED, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	if (init_pair(COLOR_YELLOW, COLOR_YELLOW, COLOR_BLACK) == ERR) {
+		return false;
+	}
+
+	return true;
+}
+
+static void
+print_deathscreen(WINDOW *const win)
+{
+	if (werase(win) == ERR) {
+		errx(1, "erase on deathscreen");
+	}
+
+	(void)box(win, 0, 0);
+	(void)mvwprintw(win, HEIGHT / 2 - 1, WIDTH / 4, "You're dead, Jim.");
+	(void)mvwprintw(win, HEIGHT / 2 + 0, WIDTH / 4,
+		"\t\t-- McCoy, stardate 3468.1");
+	(void)mvwprintw(win, HEIGHT / 2 + 2, WIDTH / 4,
+		"You've died. Game over.");
+	(void)mvwprintw(win, HEIGHT - 1, 2, "[ press any key to exit ]");
+
+	if (wrefresh(win) == ERR) {
+		errx(1, "wrefresh on deathscreen");
+	}
+
+	(void)wgetch(win);
+}
+
+static void
+print_winscreen1(WINDOW *const win)
+{
+	if (werase(win) == ERR) {
+		errx(1, "erase on winscreen1");
+	}
+
+	(void)box(win, 0, 0);
+	(void)mvwprintw(win, HEIGHT / 2 - 3, WIDTH / 12,
+		"[War] is instinctive. But the insinct can be fought. We're human");
+	(void)mvwprintw(win, HEIGHT / 2 - 2, WIDTH / 12,
+		"beings with the blood of a million savage years on our hands! But we");
+	(void)mvwprintw(win, HEIGHT / 2 - 1, WIDTH / 12,
+		"can stop it. We can admit that we're killers ... but we're not going");
+	(void)mvwprintw(win, HEIGHT / 2 + 0, WIDTH / 12,
+		"to kill today. That's all it takes! Knowing that we're not going to");
+	(void)mvwprintw(win, HEIGHT / 2 + 1, WIDTH / 12, "kill today!");
+	(void)mvwprintw(win, HEIGHT / 2 + 2, WIDTH / 12,
+		"\t\t-- Kirk, \"A Taste of Armageddon\", stardate 3193.0");
+	(void)mvwprintw(win, HEIGHT / 2 + 4, WIDTH / 12,
+		"The boss has been defeated. Game over.");
+	(void)mvwprintw(win, HEIGHT - 1, 2, "[ press any key to exit ]");
+
+	if (wrefresh(win) == ERR) {
+		errx(1, "wrefresh on winscreen1");
+	}
+
+	(void)wgetch(win);
+}
+
+static void
+print_winscreen2(WINDOW *const win)
+{
+	if (werase(win) == ERR) {
+		errx(1, "erase on winscreen2");
+	}
+
+	(void)box(win, 0, 0);
+	(void)mvwprintw(win, HEIGHT / 2 - 1, WIDTH / 4,
+		"You're still half savage. But there is hope.");
+	(void)mvwprintw(win, HEIGHT / 2 + 0, WIDTH / 4,
+		"\t\t-- Metron, stardate 3046.2");
+	(void)mvwprintw(win, HEIGHT / 2 + 2, WIDTH / 4,
+		"The boss has been defeated. Game over.");
+	(void)mvwprintw(win, HEIGHT - 1, 2, "[ press any key to exit ]");
+
+	if (wrefresh(win) == ERR) {
+		errx(1, "wrefresh on winscreen2");
+	}
+
+	(void)wgetch(win);
+}
+
+
+static bool
+is_number(std::string const &s)
+{
+	return !s.empty() && std::find_if(s.begin(), s.end(), [](char const c) {
+		return !std::isdigit(c);
+	}) == s.end();
+}
diff --git a/parse.cpp b/parse.cpp
new file mode 100644
index 0000000..95e17fd
--- /dev/null
+++ b/parse.cpp
@@ -0,0 +1,129 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <iostream>
+
+#include <err.h>
+#include <sys/stat.h>
+
+#include "gen.h"
+#include "globs.h"
+#include "y.tab.h"
+
+static char const *const NPC_FILE = "/monster_desc.txt";
+static char const *const OBJ_FILE = "/object_desc.txt";
+
+static char const *const color_map_r[] = {
+	"BLACK",
+	"BLUE",
+	"CYAN",
+	"GREEN",
+	"MAGENTA",
+	"RED",
+	"WHITE",
+	"YELLOW"
+};
+
+static char const *const type_map_r[] = {
+	"AMMUNITION",
+	"AMULET",
+	"ARMOR",
+	"BOOK",
+	"BOOTS",
+	"CLOAK",
+	"CONTAINER",
+	"FLASK",
+	"FOOD",
+	"GLOVES",
+	"GOLD",
+	"HELMET",
+	"LIGHT",
+	"OFFHAND",
+	"RANGED",
+	"RING",
+	"SCROLL",
+	"WAND",
+	"WEAPON"
+};
+
+extern int	yyparse();
+extern FILE	*yyin;
+
+std::vector<npc> npcs_parsed;
+std::vector<obj> objs_parsed;
+
+constexpr static int const line_max = 77;
+
+void
+parse_npc_file()
+{
+	struct stat st;
+	std::string const path = rlg_path() + NPC_FILE;
+
+	if (stat(path.c_str(), &st) == -1) {
+		if (errno == ENOENT) {
+			err(1, "no npc file, run with '-m' to skip "
+				"npc file parsing");
+		} else {
+			err(1, "stat npc file");
+		}
+	}
+
+	yyin = fopen(path.c_str(), "r");
+
+	if (yyin == NULL) {
+		err(1, "npc file fopen");
+	}
+
+	if (yyparse() != 0) {
+		errx(1, "yyparse npcs");
+	}
+
+	if (fclose(yyin) == EOF) {
+		err(1, "npc fclose");
+	}
+}
+
+void
+parse_obj_file()
+{
+	struct stat st;
+	std::string const path = rlg_path() + OBJ_FILE;
+
+	if (stat(path.c_str(), &st) == -1) {
+		if (errno == ENOENT) {
+			err(1, "no object file, run with '-o' to skip object "
+				"parsing");
+		} else {
+			err(1, "stat object file");
+		}
+	}
+
+	yyin = fopen(path.c_str(), "r");
+
+	if (yyin == NULL) {
+		err(1, "object file fopen");
+	}
+
+	if (yyparse() != 0) {
+		errx(1, "yyparse objs");
+	}
+
+	if (fclose(yyin) == EOF) {
+		err(1, "object fclose");
+	}
+}
diff --git a/parse.h b/parse.h
new file mode 100644
index 0000000..f113cb2
--- /dev/null
+++ b/parse.h
@@ -0,0 +1,24 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef PARSE_H
+#define PARSE_H
+
+void	parse_npc_file();
+void	parse_obj_file();
+
+#endif /* PARSE_H */
diff --git a/parse.l b/parse.l
new file mode 100644
index 0000000..2956b23
--- /dev/null
+++ b/parse.l
@@ -0,0 +1,97 @@
+%{
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "globs.h"
+#include "y.tab.h"
+
+extern bool in_n;
+extern bool in_o;
+extern npc c_npc;
+extern obj c_obj;
+%}
+
+%option nounput noyywrap
+
+S	[[:graph:]]
+
+COLORS	BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW
+ABILS	BOSS|DESTROY|ERRATIC|PASS|PICKUP|SMART|TELE|TUNNEL|UNIQ
+
+TYPES_1	AMMUNITION|AMULET|ARMOR|BOOK|BOOTS|CLOAK|CONTAINER|FLASK|FOOD|GLOVES
+TYPES_2	GOLD|HELMET|LIGHT|OFFHAND|RANGED|RING|SCROLL|WAND|WEAPON
+
+TYPES	{TYPES_1}|{TYPES_2}
+
+BOOLEAN	TRUE|FALSE
+
+%Start desc
+%%
+
+^"RLG327 MONSTER DESCRIPTION 1"$	{
+						in_n = true;
+						in_o = false;
+						return BEGIN_NPC_FILE;
+					}
+^"RLG327 OBJECT DESCRIPTION 1"$		{
+						in_o = true;
+						in_n = false;
+						return BEGIN_OBJ_FILE;
+					}
+
+^"BEGIN MONSTER"$	{ c_npc = {}; return BEGIN_NPC; }
+^"BEGIN OBJECT"$	{ c_obj = {}; return BEGIN_OBJ; }
+
+^"END"$	{
+		if (in_n) npcs_parsed.push_back(c_npc);
+		if (in_o) objs_parsed.push_back(c_obj);
+		return END;
+	}
+
+^COLOR	return COLOR;
+^DAM	return DAM;
+^NAME	return NAME;
+^RRTY	return RRTY;
+^SPEED	return SPEED;
+
+^ABIL	return ABIL;
+^HP	return HP;
+^SYMB	return SYMB;
+
+^ART	return ART;
+^ATTR	return ATTR;
+^DEF	return DEF;
+^DODGE	return DODGE;
+^HIT	return HIT;
+^TYPE	return TYPE;
+^VAL	return VAL;
+^WEIGHT	return WEIGHT;
+
+{ABILS}		{ yylval.str = yytext; return ABILS; }
+{BOOLEAN}	{ yylval.str = yytext; return BOOLEAN; }
+{COLORS}	{ yylval.str = yytext; return COLORS; }
+{TYPES}		{ yylval.str = yytext; return TYPES; }
+
+{S}+	{ yylval.str = yytext; return STR; }
+
+^DESC\n		{ BEGIN desc; return DESC; }
+<desc>^\.$	{ BEGIN 0; return DESC_END; }
+<desc>.+\n|\n	{ yylval.str = yytext; return DESC_INNER; }
+
+.|\n	;
+
+%%
diff --git a/parse.y b/parse.y
new file mode 100644
index 0000000..91bcc8e
--- /dev/null
+++ b/parse.y
@@ -0,0 +1,288 @@
+%{
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <cstring>
+#include <unordered_map>
+
+#include <err.h>
+
+#include "globs.h"
+
+extern int	yylex();
+
+static void	yyerror(char const *const);
+
+static dice	parse_dice(char *const);
+static uint64_t	parse_dice_value(char *const);
+static uint8_t	parse_rrty(char *const);
+
+bool in_n;
+bool in_o;
+
+npc c_npc;
+obj c_obj;
+
+static std::unordered_map<std::string, int> const color_map = {
+	{"BLACK", COLOR_PAIR(COLOR_BLACK)},
+	{"BLUE", COLOR_PAIR(COLOR_BLUE)},
+	{"CYAN", COLOR_PAIR(COLOR_CYAN)},
+	{"GREEN", COLOR_PAIR(COLOR_GREEN)},
+	{"MAGENTA", COLOR_PAIR(COLOR_MAGENTA)},
+	{"RED", COLOR_PAIR(COLOR_RED)},
+	{"WHITE", COLOR_PAIR(COLOR_WHITE)},
+	{"YELLOW", COLOR_PAIR(COLOR_YELLOW)}
+};
+
+static std::unordered_map<std::string, uint16_t> const ability_map = {
+	{"BOSS", BOSS},
+	{"DESTROY", DESTROY},
+	{"ERRATIC", ERRATIC},
+	{"PASS", PASS},
+	{"PICKUP", PICKUP},
+	{"SMART", SMART},
+	{"TELE", TELE},
+	{"TUNNEL", TUNNEL},
+	{"UNIQ", UNIQ}
+};
+
+static std::unordered_map<type, char> const type_symb_map = {
+	{ammunition, '/'},
+	{amulet, '"'},
+	{armor, '['},
+	{book, '?'},
+	{boots, '\\'},
+	{cloak, '('},
+	{container, '%'},
+	{flask, '!'},
+	{food, ','},
+	{gloves, '{'},
+	{gold, '$'},
+	{helmet, ']'},
+	{light, '_'},
+	{offhand, ')'},
+	{ranged, '}'},
+	{ring, '='},
+	{scroll_type, '~'},
+	{wand, '-'},
+	{weapon, '|'}
+};
+
+
+static std::unordered_map<std::string, type> const type_map = {
+	{"AMMUNITION", ammunition},
+	{"AMULET", amulet},
+	{"ARMOR", armor},
+	{"BOOK", book},
+	{"BOOTS", boots},
+	{"CLOAK", cloak},
+	{"CONTAINER", container},
+	{"FLASK", flask},
+	{"FOOD", food},
+	{"GLOVES", gloves},
+	{"GOLD", gold},
+	{"HELMET", helmet},
+	{"LIGHT", light},
+	{"OFFHAND", offhand},
+	{"RANGED", ranged},
+	{"RING", ring},
+	{"SCROLL", scroll_type},
+	{"WAND", wand},
+	{"WEAPON", weapon}
+};
+
+%}
+
+%union {
+	char *str;
+}
+
+%token BEGIN_NPC_FILE BEGIN_OBJ_FILE
+%token BEGIN_NPC BEGIN_OBJ END
+%token COLOR DAM DESC DESC_END NAME RRTY SPEED
+%token ABIL HP SYMB
+%token ART ATTR DEF DODGE HIT TYPE VAL WEIGHT
+
+%token <str> ABILS
+%token <str> BOOLEAN
+%token <str> COLORS
+%token <str> DESC_INNER
+%token <str> STR
+%token <str> TYPES
+
+%%
+
+file
+	: BEGIN_NPC_FILE npcs
+	| BEGIN_OBJ_FILE objs
+	;
+
+npcs
+	: npc
+	| npcs npc
+	;
+
+npc
+	: BEGIN_NPC npc_keywords END
+	;
+
+npc_keywords
+	: npc_keyword
+	| npc_keywords npc_keyword
+	;
+
+npc_keyword
+	: ABIL abil
+	| COLOR color
+	| DAM STR	{ c_npc.dam = parse_dice($2); }
+	| DESC desc DESC_END
+	| HP STR	{ c_npc.hp = parse_dice_value($2); }
+	| NAME name
+	| RRTY STR	{ c_npc.rrty = parse_rrty($2); }
+	| SPEED STR	{ c_npc.speed = parse_dice_value($2); }
+	| SYMB STR	{ c_npc.symb = $2[0]; }
+	;
+
+abil
+	: ABILS		{ c_npc.type |= ability_map.at($1); }
+	| abil ABILS	{ c_npc.type |= ability_map.at($2); }
+	;
+
+objs
+	: obj
+	| objs obj
+	;
+
+obj
+	: BEGIN_OBJ obj_keywords END
+	;
+
+obj_keywords
+	: obj_keyword
+	| obj_keywords obj_keyword
+	;
+
+obj_keyword
+	: ART BOOLEAN	{ c_obj.art = std::strcmp($2, "TRUE") == 0; }
+	| ATTR STR	{ c_obj.attr = parse_dice_value($2); }
+	| COLOR color
+	| DAM STR	{ c_obj.dam = parse_dice($2); }
+	| DEF STR	{ c_obj.def = parse_dice_value($2); }
+	| DESC desc DESC_END
+	| DODGE STR	{ c_obj.dodge = parse_dice_value($2); }
+	| HIT STR	{ c_obj.hit = parse_dice_value($2); }
+	| NAME name
+	| RRTY STR	{ c_obj.rrty = parse_rrty($2); }
+	| SPEED STR	{ c_obj.speed = parse_dice_value($2); }
+	| TYPE TYPES	{
+				c_obj.obj_type = type_map.at($2);
+				c_obj.symb = type_symb_map.at(c_obj.obj_type);
+			}
+	| VAL STR	{ c_obj.val = parse_dice_value($2); }
+	| WEIGHT STR	{ c_obj.val = parse_dice_value($2); }
+	;
+
+name
+	: STR		{
+				if (in_n) c_npc.name += $1;
+				if (in_o) c_obj.name += $1;
+			}
+	| name STR	{
+				if (in_n) c_npc.name = c_npc.name + " " + $2;
+				if (in_o) c_obj.name = c_obj.name + " " + $2;
+			}
+	;
+
+color
+	: COLORS	{
+				if (in_n) c_npc.color = color_map.at($1);
+				if (in_o) c_obj.color = color_map.at($1);
+			}
+	| color COLORS
+	;
+
+desc
+	: DESC_INNER		{
+					if (in_n) c_npc.desc += $1;
+					if (in_o) c_obj.desc += $1;
+				}
+	| desc DESC_INNER	{
+					if (in_n) c_npc.desc += $2;
+					if (in_o) c_obj.desc += $2;
+				}
+	;
+
+%%
+
+static void
+yyerror(char const *const s)
+{
+	errx(1, "%s", s);
+}
+
+
+static dice
+parse_dice(char *const s)
+{
+	dice d;
+	char *p, *last;
+
+	p = strtok_r(s, "+", &last);
+
+	if (p == NULL) {
+		errx(1, "dice formatted incorrectly");
+	}
+
+	d.base = (uint64_t)std::atoll(p);
+
+	p = strtok_r(NULL, "d", &last);
+
+	if (p == NULL) {
+		errx(1, "dice formatted incorrectly");
+	}
+
+	d.dice = (uint64_t)std::atoll(p);
+
+	p = strtok_r(NULL, "\0", &last);
+
+	if (p == NULL) {
+		errx(1, "dice format missing '+'");
+	}
+
+	d.sides = (uint64_t)std::atoll(p);
+
+	return d;
+}
+
+static uint64_t
+parse_dice_value(char *const s)
+{
+	dice const d = parse_dice(s);
+	return rr.rand_dice<uint64_t>(d.base, d.dice, d.sides);
+}
+
+static uint8_t
+parse_rrty(char *const s)
+{
+	uint8_t const rrty = (uint8_t)std::atoi(s);
+
+	if (rrty == 0 || rrty > 100) {
+		errx(1, "rrty '%d' out of bounds [1, 100]", rrty);
+	}
+
+	return rrty;
+}
diff --git a/rand.cpp b/rand.cpp
new file mode 100644
index 0000000..ce11837
--- /dev/null
+++ b/rand.cpp
@@ -0,0 +1,44 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "rand.h"
+
+ranged_random rr;
+
+ranged_random::ranged_random()
+{
+	seed = std::random_device{}();
+	gen.seed(seed);
+}
+
+ranged_random::ranged_random(std::string const &s)
+{
+	long unsigned int hash = 5381;
+
+	for(auto const c : s) {
+		hash = ((hash << 5)) ^ c;
+	}
+
+	seed = hash;
+	gen.seed(seed);
+}
+
+ranged_random::ranged_random(long unsigned int const s)
+{
+	seed = s;
+	gen.seed(seed);
+}
diff --git a/rand.h b/rand.h
new file mode 100644
index 0000000..dd73ddf
--- /dev/null
+++ b/rand.h
@@ -0,0 +1,54 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef RAND_H
+#define RAND_H
+
+#include <random>
+#include <string>
+
+class ranged_random {
+	std::mt19937 gen;
+public:
+	long unsigned int seed;
+
+	ranged_random();
+
+	explicit ranged_random(std::string const &);
+
+	explicit ranged_random(long unsigned int const);
+
+	template<typename T> T
+	rrand(T a, T b)
+	{
+		std::uniform_int_distribution<T> dis(a, b);
+
+		return dis(gen);
+	}
+
+	template<typename T> T
+	rand_dice(T base, T dice, T sides)
+	{
+		T total = base;
+		for (size_t i = 0; i < dice; ++i) {
+			total = static_cast<T>(total + rrand<T>(1, sides));
+		}
+		return total;
+	}
+};
+
+#endif /* RAND_H */
diff --git a/turn.cpp b/turn.cpp
new file mode 100644
index 0000000..d4cab1b
--- /dev/null
+++ b/turn.cpp
@@ -0,0 +1,1649 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#include <algorithm>
+#include <cinttypes>
+#include <functional>
+#include <limits>
+#include <new>
+#include <queue>
+#include <sstream>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include <err.h>
+
+#include "dijk.h"
+#include "globs.h"
+#include "turn.h"
+
+static bool	valid_thing(uint8_t const, uint8_t const);
+
+static double		distance(uint8_t const, uint8_t const, uint8_t const, uint8_t const);
+static unsigned int	subu32(unsigned int const, unsigned int const);
+static uint64_t		subu64(uint64_t const, uint64_t const);
+
+static bool	pc_visible(int const, int const);
+
+static void	npc_obj_or_tile(WINDOW *const, uint8_t const, uint8_t const);
+
+static uint64_t	effective_dam();
+static uint64_t	combat(npc &, npc &);
+
+static void	move_redraw(WINDOW *const, npc &, uint8_t const, uint8_t const);
+static void	move_logic(WINDOW *const, npc &, uint8_t const, uint8_t const);
+static void	move_tunnel(WINDOW *const, npc &, uint8_t const, uint8_t const);
+
+static void	move_straight(WINDOW *const, npc &);
+static void	move_dijk_nontunneling(WINDOW *const, npc &);
+static void	move_dijk_tunneling(WINDOW *const, npc &);
+
+static std::optional<std::pair<uint8_t, uint8_t>>	gen_npc();
+static std::optional<std::pair<uint8_t, uint8_t>>	gen_obj();
+
+static void	npc_list(WINDOW *const, std::vector<npc *> const &);
+
+static void	defog(WINDOW *const);
+
+static void	crosshair(WINDOW *const, uint8_t const, uint8_t const);
+static bool	inspect(WINDOW *const, bool const);
+
+static bool	viewable(int const, int const);
+static void	pc_viewbox(WINDOW *const, int const);
+
+static void	try_carry(uint8_t const, uint8_t const);
+
+static void	equip_list(WINDOW *const, bool const);
+
+static void	carry_to_equip(int const);
+static void	equip_to_carry(int const, std::optional<std::string> &);
+
+static void	thing_details(WINDOW *const, dungeon_thing const &);
+
+enum pc_action {
+	PC_DEFOG,
+	PC_NEXT,
+	PC_NONE,
+	PC_NPC_LIST,
+	PC_QUIT,
+	PC_RETRY,
+	PC_TELE
+};
+
+static enum pc_action	turn_npc(WINDOW *const, WINDOW *const, npc &);
+static enum pc_action	turn_pc(WINDOW *const, WINDOW *const, npc &);
+
+enum carry_action {
+	CARRY_DROP,
+	CARRY_INSPECT,
+	CARRY_LIST,
+	CARRY_REMOVE,
+	CARRY_WEAR
+};
+
+static void	carry_list(WINDOW *const, carry_action const);
+
+struct equip {
+	std::optional<obj>	amulet;
+	std::optional<obj>	armor;
+	std::optional<obj>	boots;
+	std::optional<obj>	cloak;
+	std::optional<obj>	gloves;
+	std::optional<obj>	helmet;
+	std::optional<obj>	light;
+	std::optional<obj>	offhand;
+	std::optional<obj>	ranged;
+	std::optional<obj>	ring_left;
+	std::optional<obj>	ring_right;
+	std::optional<obj>	weapon;
+};
+
+static char const *const type_map_name[] = {
+	"ammunition",
+	"amulet",
+	"armor",
+	"book",
+	"boots",
+	"cloak",
+	"container",
+	"flask",
+	"food",
+	"gloves",
+	"gold",
+	"helmet",
+	"light",
+	"offhand",
+	"ranged",
+	"ring",
+	"scroll",
+	"wand",
+	"weapon"
+};
+
+static bool type_map_equip[] = {
+	false,
+	true,
+	true,
+	false,
+	true,
+	true,
+	false,
+	false,
+	false,
+	true,
+	false,
+	true,
+	true,
+	true,
+	true,
+	true,
+	false,
+	false,
+	true
+};
+
+struct compare_npc {
+	bool constexpr
+	operator() (npc const &x, npc const &y) const
+	{
+		return x.turn > y.turn;
+	}
+};
+
+/* minimum distance from the PC an NPC can be placed */
+static double constexpr CUTOFF = 4.0;
+static int constexpr PERSISTANCE = 5;
+static int constexpr KEY_ESC = 27;
+static int constexpr DEFAULT_LUMINANCE = 5;
+static unsigned int constexpr RETRIES = 150;
+static int constexpr PC_CARRY_MAX = 10;
+
+static std::optional<obj> pc_carry[PC_CARRY_MAX];
+static equip pc_equip;
+
+enum turn_exit
+turn_engine(WINDOW *const win, unsigned int const numnpcs,
+	unsigned int const numobjs)
+{
+	std::priority_queue<npc, std::vector<std::reference_wrapper<npc>>, compare_npc> heap;
+	std::vector<npc *> npcs;
+	std::vector<obj *> objs;
+	unsigned int real_num = 0;
+
+	WINDOW *sep;
+
+	uint64_t turn;
+	enum turn_exit ret = TURN_NONE;
+
+	try {
+		npcs.resize(numnpcs);
+		objs.resize(numobjs);
+	} catch (std::bad_alloc const &) {
+		err(1, "resize npcs and objs");
+	}
+
+	tiles[player.y][player.x].n = &player;
+
+	wattron(win, player.color);
+	(void)mvwaddch(win, player.y, player.x, player.symb);
+	wattroff(win, player.color);
+
+	heap.push(player);
+
+	for (auto &n : npcs) {
+		size_t i;
+		unsigned int retries = 0;
+		do {
+			i = rr.rrand<size_t>(0, npcs_parsed.size() - 1);
+			retries++;
+		} while (retries < RETRIES && (npcs_parsed[i].done
+			|| npcs_parsed[i].rrty <= rr.rrand<uint8_t>(0, 99)));
+
+		if (retries == RETRIES) {
+			break;
+		}
+
+		std::optional<std::pair<uint8_t, uint8_t>> coords = gen_npc();
+
+		if (!coords.has_value()) {
+			break;
+		}
+
+		real_num++;
+
+		n = new npc(npcs_parsed[i]);
+
+		if (n->type & UNIQ) {
+			n->done = true;
+			npcs_parsed[i].done = true;
+		}
+
+		n->x = coords->first;
+		n->y = coords->second;
+
+		tiles[n->y][n->x].n = n;
+
+		heap.push(*n);
+	}
+
+	if (real_num != numnpcs) {
+		npcs.resize(real_num);
+	}
+
+	real_num = 0;
+
+	for (auto &o : objs) {
+		size_t i = 0;
+		unsigned int retries = 0;
+		do {
+			i = rr.rrand<size_t>(0, objs_parsed.size() - 1);
+			retries++;
+		} while (retries < RETRIES && (objs_parsed[i].done
+			|| objs_parsed[i].rrty <= rr.rrand<uint8_t>(0, 99)));
+
+		if (retries == RETRIES) {
+			break;
+		}
+
+		std::optional<std::pair<uint8_t, uint8_t>> coords = gen_obj();
+
+		if (!coords.has_value()) {
+			break;
+		}
+
+		real_num++;
+
+		o = new obj(objs_parsed[i]);
+
+		if (o->art) {
+			o->done = true;
+			objs_parsed[i].done = true;
+		}
+
+		o->x = coords->first;
+		o->y = coords->second;
+
+		tiles[o->y][o->x].o = o;
+	}
+
+	if (real_num != numobjs) {
+		objs.resize(real_num);
+	}
+
+	dijkstra();
+
+	if ((sep = newwin(HEIGHT, WIDTH, 0, 0)) == NULL) {
+		errx(1, "newwin sep");
+	}
+
+	if (keypad(sep, true) == ERR) {
+		errx(1, "keypad sep");
+	}
+
+	(void)box(sep, 0, 0);
+
+	pc_viewbox(win, DEFAULT_LUMINANCE);
+
+	(void)mvwprintw(win, HEIGHT - 1, 2,
+		"[ hp: %" PRIu64 " ]", player.hp);
+
+	while (!heap.empty()) {
+		npc &n = heap.top();
+		heap.pop();
+
+		if (n.type & PLAYER_TYPE && wrefresh(win) == ERR) {
+			errx(1, "turn_engine wrefresh");
+		}
+
+		if (n.hp == 0) {
+			if (n.type & PLAYER_TYPE) {
+				ret = TURN_DEATH;
+				goto exit;
+			} else if (n.type & BOSS) {
+				ret = TURN_WIN;
+				goto exit;
+			} else {
+				continue;
+			}
+		}
+
+		turn = n.turn + 1;
+		n.turn = turn + 1000/n.speed;
+
+		retry:
+		if (touchwin(win) == ERR) {
+			errx(1, "touchwin");
+		}
+
+		switch(turn_npc(win, sep, n)) {
+		case PC_DEFOG:
+			defog(sep);
+			goto retry;
+		case PC_NEXT:
+			ret = TURN_NEXT;
+			goto exit;
+		case PC_NONE:
+			break;
+		case PC_NPC_LIST:
+			npc_list(sep, npcs);
+			goto retry;
+		case PC_QUIT:
+			ret = TURN_QUIT;
+			goto exit;
+		case PC_RETRY:
+			goto retry;
+		case PC_TELE:
+			if (inspect(win, true)) {
+				break;
+			} else {
+				goto retry;
+			}
+		}
+
+		heap.push(n);
+	}
+
+	exit:
+
+	for (auto &n : npcs) {
+		delete n;
+	}
+
+	for (auto &o : objs) {
+		delete o;
+	}
+
+	if (delwin(sep) == ERR) {
+		errx(1, "turn_engine delwin sep");
+	}
+
+	return ret;
+}
+
+static bool
+valid_thing(uint8_t const y, uint8_t const x)
+{
+	if (tiles[y][x].h != 0) {
+		return false;
+	}
+
+	return distance(player.x, player.y, x, y) > CUTOFF;
+}
+
+static double
+distance(uint8_t const x0, uint8_t const y0, uint8_t const x1, uint8_t const y1)
+{
+	int const dx = x1 - x0;
+	int const dy = y1 - y0;
+	return std::sqrt(dx * dx + dy * dy);
+}
+
+/*
+ * From my branchfree saturating arithmetic library.
+ * See: https://github.com/esote/bsa
+ */
+static unsigned int
+subu32(unsigned int const a, unsigned int const b)
+{
+	unsigned int res = a - b;
+	res &= (unsigned int) (-(unsigned int) (res <= a));
+	return res;
+}
+
+static uint64_t
+subu64(uint64_t const a, uint64_t const b)
+{
+	uint64_t res = a - b;
+	res &= (uint64_t) (-(uint64_t) (res <= a));
+	return res;
+}
+
+/* Bresenham's line algorithm */
+static bool
+pc_visible(int const x1, int const y1)
+{
+	int x0 = player.x;
+	int y0 = player.y;
+
+	int const dx = std::abs(x1 - x0);
+	int const dy = std::abs(y1 - y0);
+	int const sx = x0 < x1 ? 1 : -1;
+	int const sy = y0 < y1 ? 1 : -1;
+
+	int err = (dx > dy ? dx : -dy) / 2;
+
+	while (1) {
+		if (tiles[y0][x0].h != 0) {
+			return false;
+		}
+
+		if (x0 == x1 && y0 == y1) {
+			break;
+		}
+
+		int e2 = err;
+
+		if (e2 > -dx) {
+			err -= dy;
+			x0 += sx;
+		}
+
+		if (e2 < dy) {
+			err += dx;
+			y0 += sy;
+		}
+	}
+
+	return true;
+}
+
+static void
+npc_obj_or_tile(WINDOW *const win, uint8_t const y, uint8_t const x)
+{
+	if (tiles[y][x].n != NULL) {
+		wattron(win, tiles[y][x].n->color);
+		(void)mvwaddch(win, y, x, tiles[y][x].n->symb);
+		wattroff(win, tiles[y][x].n->color);
+	} else if (tiles[y][x].o != NULL) {
+		wattron(win, tiles[y][x].o->color);
+		(void)mvwaddch(win, y, x, tiles[y][x].o->symb);
+		wattroff(win, tiles[y][x].o->color);
+	} else {
+		(void)mvwaddch(win, y, x, tiles[y][x].c);
+	}
+}
+
+static uint64_t
+effective_dam()
+{
+	int const length = 12;
+	std::optional<obj> const equip[] = {
+		pc_equip.amulet,
+		pc_equip.armor,
+		pc_equip.boots,
+		pc_equip.cloak,
+		pc_equip.gloves,
+		pc_equip.helmet,
+		pc_equip.light,
+		pc_equip.offhand,
+		pc_equip.ranged,
+		pc_equip.ring_left,
+		pc_equip.ring_right,
+		pc_equip.weapon
+	};
+
+	uint64_t dam = rr.rand_dice<uint64_t>(player.dam.base, player.dam.dice,
+		player.dam.sides);
+
+	for (int i = 0; i < length; ++i) {
+		if (equip[i].has_value()) {
+			dam += rr.rand_dice<uint64_t>(equip[i]->dam.base,
+				equip[i]->dam.dice, equip[i]->dam.sides);
+		}
+	}
+
+	return dam;
+}
+
+static uint64_t
+combat(npc &n1, npc &n2)
+{
+	uint64_t n1_dam = rr.rand_dice<uint64_t>(n1.dam.base, n1.dam.dice,
+		n1.dam.sides);
+	uint64_t n2_hp = n2.hp;
+
+	if (n1.type & PLAYER_TYPE) {
+		n1_dam = effective_dam();
+	} else {
+		n2_hp = player.hp;
+	}
+
+	n2.hp = subu64(n2_hp, n1_dam);
+
+	return n1_dam;
+}
+
+static void
+move_redraw(WINDOW *const win, npc &n, uint8_t const y, uint8_t const x)
+{
+	tiles[n.y][n.x].n = NULL;
+	tiles[y][x].n = &n;
+
+	if (tiles[n.y][n.x].v || n.type & PLAYER_TYPE) {
+		npc_obj_or_tile(win, n.y, n.x);
+	}
+
+	if (tiles[y][x].v) {
+		wattron(win, n.color);
+		(void)mvwaddch(win, y, x, n.symb);
+		wattroff(win, n.color);
+	}
+
+	n.y = y;
+	n.x = x;
+}
+
+static void
+move_logic(WINDOW *const win, npc &n, uint8_t const y, uint8_t const x)
+{
+	if (n.y == y && n.x == x) {
+		return;
+	}
+
+	/* move to empty tile */
+	if (tiles[y][x].n == NULL) {
+		move_redraw(win, n, y, x);
+		return;
+	}
+
+	/* npc-pc combat */
+	if (n.type & PLAYER_TYPE || tiles[y][x].n->type & PLAYER_TYPE) {
+		uint64_t dam = combat(n, *tiles[y][x].n);
+
+		(void)box(win, 0, 0);
+		(void)mvwprintw(win, HEIGHT - 1, 2,
+			"[ hp: %" PRIu64 " ]", player.hp);
+
+		if (n.type & PLAYER_TYPE) {
+			(void)mvwprintw(win, HEIGHT - 1, WIDTH / 4,
+				"[ delt %" PRIu64 " damage ]", dam);
+		} else {
+			(void)mvwprintw(win, HEIGHT - 1, WIDTH / 4,
+				"[ received %" PRIu64 " damage ]", dam);
+		}
+
+		if (tiles[y][x].n->hp == 0) {
+			tiles[y][x].n->dead = true;
+			tiles[y][x].n = NULL;
+			npc_obj_or_tile(win, y, x);
+		}
+
+		return;
+	}
+
+	/* npc-to-npc */
+	for (int i = -1; i <= 1; ++i) {
+		for (int j = -1; j <= 1; ++j) {
+			uint8_t tx = (uint8_t)(tiles[y][x].n->x + i);
+			uint8_t ty = (uint8_t)(tiles[y][x].n->y + j);
+
+			if (tx == 0 || ty == 0 || tx >= WIDTH - 1
+				|| ty >= HEIGHT - 1) {
+				continue;
+			}
+
+			if (tiles[ty][tx].n == NULL && tiles[ty][tx].h == 0) {
+				/* move to tiles[y][x].n to ty, tx */
+				move_redraw(win, *tiles[y][x].n, ty, tx);
+				move_redraw(win, n, y, x);
+				return;
+			}
+		}
+	}
+
+	/* swap tiles[y][x].n with n */
+	move_redraw(win, *tiles[y][x].n, n.y, n.x);
+	move_redraw(win, n, y, x);
+}
+
+static void
+move_tunnel(WINDOW *const win, npc &n, uint8_t const y, uint8_t const x)
+{
+	if (tiles[y][x].h == UINT8_MAX) {
+		return;
+	}
+
+	tiles[y][x].h = (uint8_t)subu32(tiles[y][x].h, TUNNEL_STRENGTH);
+
+	dijkstra();
+
+	if (tiles[y][x].h != 0) {
+		return;
+	}
+
+	if (tiles[y][x].c == ROCK) {
+		tiles[y][x].c = CORRIDOR;
+	}
+
+	move_logic(win, n, y, x);
+}
+
+static void
+move_straight(WINDOW *const win, npc &n)
+{
+	double min = std::numeric_limits<double>::max();
+	uint8_t minx = n.x;
+	uint8_t miny = n.y;
+
+	for (int i = -1; i <= 1; ++i) {
+		for (int j = -1; j <= 1; ++j) {
+			uint8_t x = (uint8_t)(n.x + i);
+			uint8_t y = (uint8_t)(n.y + j);
+
+			if (!(n.type & TUNNEL) && tiles[y][x].h != 0) {
+				continue;
+			}
+
+			double dist = distance(player.x, player.y, x, y);
+
+			if (dist < min) {
+				min = dist;
+				minx = x;
+				miny = y;
+			}
+		}
+	}
+
+	if (n.type & TUNNEL) {
+		move_tunnel(win, n, miny, minx);
+	} else {
+		move_logic(win, n, miny, minx);
+	}
+}
+
+static void
+move_dijk_nontunneling(WINDOW *const win, npc &n)
+{
+	int32_t min_d = tiles[n.y][n.x].d;
+	uint8_t minx = n.x;
+	uint8_t miny = n.y;
+
+	for (int i = -1; i <= 1; ++i) {
+		for (int j = -1; j <= 1; ++j) {
+			uint8_t x = (uint8_t)(n.x + i);
+			uint8_t y = (uint8_t)(n.y + j);
+
+
+			if (tiles[y][x].h != 0) {
+				continue;
+			}
+
+			if (tiles[y][x].d < min_d) {
+				min_d = tiles[y][x].d;
+				minx = x;
+				miny = y;
+			}
+		}
+	}
+
+	move_logic(win, n, miny, minx);
+}
+
+static void
+move_dijk_tunneling(WINDOW *const win, npc &n)
+{
+	int32_t min_dt = tiles[n.y][n.x].dt;
+	uint8_t minx = n.x;
+	uint8_t miny = n.y;
+
+	for (int i = -1; i <= 1; ++i) {
+		for (int j = -1; j <= 1; ++j) {
+			uint8_t x = (uint8_t)(n.x + i);
+			uint8_t y = (uint8_t)(n.y + j);
+
+			if (tiles[y][x].dt < min_dt) {
+				min_dt = tiles[y][x].dt;
+				minx = x;
+				miny = y;
+			}
+		}
+	}
+
+	move_tunnel(win, n, miny, minx);
+}
+
+static std::optional<std::pair<uint8_t, uint8_t>>
+gen_npc()
+{
+	uint8_t x, y;
+	size_t retries = 0;
+
+	do {
+		x = rr.rrand<uint8_t>(1, WIDTH - 2);
+		y = rr.rrand<uint8_t>(1, HEIGHT - 2);
+		retries++;
+	} while (retries < RETRIES && (!valid_thing(y, x)
+		|| tiles[y][x].n != NULL));
+
+	if (retries == RETRIES) {
+		return {};
+	}
+
+	return std::make_pair(x, y);
+}
+
+static std::optional<std::pair<uint8_t, uint8_t>>
+gen_obj()
+{
+	uint8_t x, y;
+	size_t retries = 0;
+
+	do {
+		x = rr.rrand<uint8_t>(1, WIDTH - 2);
+		y = rr.rrand<uint8_t>(1, HEIGHT - 2);
+		retries++;
+	} while (retries < RETRIES && (!valid_thing(y, x)
+		|| tiles[y][x].o != NULL));
+
+	if (retries == RETRIES) {
+		return {};
+	}
+
+	return std::make_pair(x, y);
+}
+
+static enum pc_action
+turn_npc(WINDOW *const win, WINDOW *const sep, npc &n)
+{
+	if (n.type & PLAYER_TYPE) {
+		pc_viewbox(win, DEFAULT_LUMINANCE);
+		return turn_pc(win, sep, n);
+	}
+
+	if (n.type & ERRATIC && rr.rrand<int>(0, 1) == 0) {
+		uint8_t y, x;
+
+		do {
+			y = (uint8_t)(n.y + rr.rrand<int>(-1, 1));
+			x = (uint8_t)(n.x + rr.rrand<int>(-1, 1));
+		} while (!(n.type & TUNNEL) && tiles[y][x].h != 0);
+
+		if (n.type & TUNNEL) {
+			move_tunnel(win, n, y, x);
+		} else {
+			move_logic(win, n, y, x);
+		}
+
+		return PC_NONE;
+	}
+
+	uint16_t const basic_type = n.type & 0xF;
+
+	switch(basic_type) {
+	case 0x0:
+	case 0x8:
+	case 0x4:
+	case 0xC:
+		/* straight line and tunnel if can see player */
+		if (pc_visible(n.x, n.y)) {
+			move_straight(win, n);
+		}
+		break;
+	case 0x2:
+	case 0xA:
+	case 0x6:
+	case 0xE:
+		/* straight line and tunnel, telepathic towards player */
+		move_straight(win, n);
+		break;
+	case 0x1:
+	case 0x9:
+	case 0x3:
+	case 0xB:
+		/* nontunneling dijk, remembered location or telepathic */
+		if (n.type & TELE || pc_visible(n.x, n.y)) {
+			n.p_count = PERSISTANCE;
+		}
+
+		if (n.p_count != 0) {
+			move_dijk_nontunneling(win, n);
+			n.p_count--;
+		}
+		break;
+	case 0x5:
+	case 0xD:
+	case 0x7:
+	case 0xF:
+		/* tunneling dijk, remembered location or telepathic */
+		if (n.type & TELE || pc_visible(n.x, n.y)) {
+			n.p_count = PERSISTANCE;
+		}
+
+		if (n.p_count != 0) {
+			move_dijk_tunneling(win, n);
+			n.p_count--;
+		}
+		break;
+	default:
+		errx(1, "turn_npc invalid npc type %d", n.type);
+	}
+
+	return PC_NONE;
+}
+
+static enum pc_action
+turn_pc(WINDOW *const win, WINDOW *const sep, npc &n)
+{
+	uint8_t y = n.y;
+	uint8_t x = n.x;
+	bool exit = false;
+
+	while (!exit) {
+		exit = true;
+		switch(wgetch(win)) {
+		case ERR:
+			errx(1, "turn_pc wgetch ERR");
+			break;
+		case KEY_HOME:
+		case KEY_A1:
+		case '7':
+		case 'y':
+			/* up left */
+			y--;
+			x--;
+			break;
+		case KEY_UP:
+		case '8':
+		case 'k':
+			/* up */
+			y--;
+			break;
+		case KEY_PPAGE:
+		case KEY_A3:
+		case '9':
+		case 'u':
+			/* up right */
+			y--;
+			x++;
+			break;
+		case KEY_RIGHT:
+		case '6':
+		case 'l':
+			/* right */
+			x++;
+			break;
+		case KEY_NPAGE:
+		case KEY_C3:
+		case '3':
+		case 'n':
+			/* down right */
+			y++;
+			x++;
+			break;
+		case KEY_DOWN:
+		case '2':
+		case 'j':
+			/* down */
+			y++;
+			break;
+		case KEY_END:
+		case KEY_C1:
+		case '1':
+		case 'b':
+			/* down left */
+			y++;
+			x--;
+			break;
+		case KEY_LEFT:
+		case '4':
+		case 'h':
+			/* left */
+			x--;
+			break;
+		case KEY_B2:
+		case ' ':
+		case '5':
+		case '.':
+			/* rest */
+			//return PC_NONE;
+			break;
+		case '>':
+			/* go down stairs */
+			if (tiles[y][x].c == STAIR_DN) {
+				return PC_NEXT;
+			} else {
+				exit = false;
+			}
+			break;
+		case '<':
+			/* go up stairs */
+			if (tiles[y][x].c == STAIR_UP) {
+				return PC_NEXT;
+			} else {
+				exit = false;
+			}
+			break;
+		case 'm':
+			return PC_NPC_LIST;
+		case 'Q':
+		case 'q':
+			return PC_QUIT;
+		case 'f':
+			return PC_DEFOG;
+		case 'g':
+			return PC_TELE;
+		case 'i':
+			carry_list(sep, CARRY_LIST);
+			return PC_RETRY;
+		case 'e':
+			equip_list(sep, false);
+			return PC_RETRY;
+		case 'w':
+			carry_list(sep, CARRY_WEAR);
+			return PC_RETRY;
+		case 't':
+			equip_list(sep, true);
+			return PC_RETRY;
+		case 'd':
+			carry_list(sep, CARRY_DROP);
+			return PC_RETRY;
+		case 'x':
+			carry_list(sep, CARRY_REMOVE);
+			return PC_RETRY;
+		case 'L':
+			inspect(win, false);
+			return PC_RETRY;
+		case 'I':
+			carry_list(sep, CARRY_INSPECT);
+			return PC_RETRY;
+		default:
+			exit = false;
+		}
+	}
+
+	if (tiles[y][x].h == 0) {
+		move_logic(win, n, y, x);
+		try_carry(y, x);
+		dijkstra();
+	}
+
+	return PC_NONE;
+}
+
+static void
+npc_list(WINDOW *const nwin, std::vector<npc *> const &npcs)
+{
+	std::vector<npc>::size_type cpos = 0;
+
+	while (1) {
+		if (werase(nwin) == ERR) {
+			errx(1, "npc_list erase");
+		}
+
+		(void)box(nwin, 0, 0);
+
+		(void)mvwprintw(nwin, HEIGHT - 1, 2,
+			"[ arrow keys to scroll; ESC to exit ]");
+
+		std::size_t i;
+		for (i = 0; i < HEIGHT - 2 && i + cpos < npcs.size(); ++i) {
+			npc *n = npcs[i + cpos];
+
+			if (n->dead) {
+				(void)mvwprintw(nwin, static_cast<int>(i + 1U),
+					2, "%u.\t'%c'\t(dead)\t\t%s", i + cpos,
+					n->symb, n->name.c_str());
+				continue;
+			}
+
+			int dx = player.x - n->x;
+			int dy = player.y - n->y;
+
+			(void)mvwprintw(nwin, static_cast<int>(i + 1U), 2,
+				"%u.\t'%c'\t%d %s and %d %s\t%s", i + cpos,
+				n->symb, abs(dy), dy > 0 ? "north" : "south",
+				abs(dx), dx > 0 ? "west" : "east",
+				n->name.c_str());
+		}
+
+		for (; i < HEIGHT - 2; ++i) {
+			(void)mvwaddch(nwin, static_cast<int>(i + 1U), 2, '~');
+		}
+
+		if (wrefresh(nwin) == ERR) {
+			errx(1, "npc_list wrefresh");
+		}
+
+		switch(wgetch(nwin)) {
+		case ERR:
+			errx(1, "npc_list wgetch ERR");
+			return;
+		case KEY_UP:
+			if (--cpos > npcs.size()) {
+				cpos = 0;
+			}
+			break;
+		case KEY_DOWN:
+			if (++cpos > npcs.size() - 1) {
+				cpos = npcs.size() - 1;
+			}
+			break;
+		case KEY_ESC:
+			return;
+		default:
+			break;
+		}
+	}
+}
+
+static void
+defog(WINDOW *const win)
+{
+	for (uint8_t x = 1; x < WIDTH - 1; ++x) {
+		for (uint8_t y = 1; y < HEIGHT - 1; ++y) {
+			npc_obj_or_tile(win, y, x);
+		}
+	}
+
+	wattron(win, player.color);
+	(void)mvwaddch(win, player.y, player.x, player.symb);
+	wattroff(win, player.color);
+
+	(void)mvwprintw(win, HEIGHT - 1, 2, "[ press any key to exit ]");
+
+	if (wrefresh(win) == ERR) {
+		errx(1, "defog wrefresh");
+	}
+
+	(void)wgetch(win);
+}
+
+static void
+crosshair(WINDOW *const win, uint8_t const y, uint8_t const x)
+{
+	for (int i = 1; i < HEIGHT - 1; ++i) {
+		if (i != y) {
+			(void)mvwaddch(win, i, x, ACS_VLINE);
+		}
+	}
+
+	for (int i = 1; i < WIDTH - 1; ++i) {
+		if (i != x) {
+			(void)mvwaddch(win, y, i, ACS_HLINE);
+		}
+	}
+
+	(void)mvwaddch(win, y, 0, ACS_LTEE);
+	(void)mvwaddch(win, y, WIDTH - 1, ACS_RTEE);
+	(void)mvwaddch(win, 0, x, ACS_TTEE);
+	(void)mvwaddch(win, HEIGHT - 1, x, ACS_BTEE);
+
+	(void)mvwaddch(win, y + 1, x + 0, ACS_TTEE);
+	(void)mvwaddch(win, y - 1, x + 0, ACS_BTEE);
+	(void)mvwaddch(win, y + 0, x - 1, ACS_RTEE);
+	(void)mvwaddch(win, y + 0, x + 1, ACS_LTEE);
+
+	(void)mvwaddch(win, y - 1, x - 1, ACS_ULCORNER);
+	(void)mvwaddch(win, y - 1, x + 1, ACS_URCORNER);
+	(void)mvwaddch(win, y + 1, x - 1, ACS_LLCORNER);
+	(void)mvwaddch(win, y + 1, x + 1, ACS_LRCORNER);
+}
+
+static bool
+inspect(WINDOW *const win, bool const teleport)
+{
+	WINDOW *twin;
+	uint8_t y = player.y;
+	uint8_t x = player.x;
+	bool ret = true;
+
+	while (1) {
+		if ((twin = dupwin(win)) == NULL) {
+			errx(1, "inspect dupwin");
+		}
+
+		if (touchwin(twin) == ERR) {
+			errx(1, "inspect touchwin");
+		}
+
+		crosshair(twin, y, x);
+
+		if (teleport) {
+			(void)mvwprintw(twin, HEIGHT - 1, 2,
+				"[ PC control keys; 'r' for random location; "
+				"'g' or 't' to teleport; ESC to exit ]");
+		} else {
+			(void)mvwprintw(twin, HEIGHT - 1, 2,
+				"[ PC control keys; 'g' or 't' to inspect; "
+				"ESC to exit ]");
+		}
+
+
+		if (wrefresh(twin) == ERR) {
+			errx(1, "inspect wrefresh");
+		}
+
+		switch(wgetch(win)) {
+		case ERR:
+			errx(1, "inspect wgetch ERR");
+			break;
+		case KEY_HOME:
+		case KEY_A1:
+		case '7':
+		case 'y':
+			/* up left */
+			y--;
+			x--;
+			break;
+		case KEY_UP:
+		case '8':
+		case 'k':
+			/* up */
+			y--;
+			break;
+		case KEY_PPAGE:
+		case KEY_A3:
+		case '9':
+		case 'u':
+			/* up right */
+			y--;
+			x++;
+			break;
+		case KEY_RIGHT:
+		case '6':
+		case 'l':
+			/* right */
+			x++;
+			break;
+		case KEY_NPAGE:
+		case KEY_C3:
+		case '3':
+		case 'n':
+			/* down right */
+			y++;
+			x++;
+			break;
+		case KEY_DOWN:
+		case '2':
+		case 'j':
+			/* down */
+			y++;
+			break;
+		case KEY_END:
+		case KEY_C1:
+		case '1':
+		case 'b':
+			/* down left */
+			y++;
+			x--;
+			break;
+		case KEY_LEFT:
+		case '4':
+		case 'h':
+			/* left */
+			x--;
+			break;
+		case 'r':
+			if (teleport) {
+				/* random teleport location */
+				x = rr.rrand<uint8_t>(2, WIDTH - 1);
+				y = rr.rrand<uint8_t>(2, HEIGHT - 1);
+			}
+
+			break;
+		case 't':
+		case 'g':
+			if (teleport && tiles[y][x].n == NULL) {
+				/* complete teleport */
+				tiles[y][x].v = true;
+				move_logic(win, player, y, x);
+				goto exit;
+			}
+
+			if (!teleport && tiles[y][x].n != NULL) {
+				thing_details(twin, *tiles[y][x].n);
+			}
+
+			break;
+		case KEY_ESC:
+			ret = false;
+			goto exit;
+		default:
+			break;
+		}
+
+		if (x >= WIDTH - 1) {
+			x = WIDTH - 2;
+		} else if (x < 1) {
+			x = 1;
+		}
+
+		if (y >= HEIGHT - 1) {
+			y = HEIGHT - 2;
+		} else if (y < 1) {
+			y = 1;
+		}
+	}
+
+	exit:
+
+	if (delwin(twin) == ERR) {
+		errx(1, "inspect delwin");
+	}
+
+	return ret;
+}
+
+static bool
+viewable(int const y, int const x)
+{
+	return !tiles[y][x].v && pc_visible(x, y)
+		&& (pc_visible(x - 1, y + 0)
+		|| pc_visible(x + 1, y + 0)
+		|| pc_visible(x + 0, y - 1)
+		|| pc_visible(x + 0, y + 1)
+		|| pc_visible(x + 1, y + 1)
+		|| pc_visible(x - 1, y - 1)
+		|| pc_visible(x - 1, y + 1)
+		|| pc_visible(x + 1, y - 1));
+}
+
+static void
+pc_viewbox(WINDOW *const win, int const lum)
+{
+	uint8_t const start_x = (uint8_t)subu32(player.x + 1, lum);
+	uint8_t const end_x = (uint8_t)(player.x + lum);
+
+	uint8_t const start_y = (uint8_t)subu32(player.y + 1, lum);
+	uint8_t const end_y = (uint8_t)(player.y + lum);
+
+	for (uint8_t i = start_x; i <= end_x && i < WIDTH - 1; ++i) {
+		for (uint8_t j = start_y; j <= end_y && j < HEIGHT - 1; ++j) {
+			if (!viewable(j, i)) {
+				continue;
+			}
+
+			tiles[j][i].v = true;
+			npc_obj_or_tile(win, j, i);
+		}
+	}
+}
+
+static void
+try_carry(uint8_t const y, uint8_t const x)
+{
+	if (tiles[y][x].o == NULL) {
+		return;
+	}
+
+	for (int i = 0; i < PC_CARRY_MAX; ++i) {
+		if (!pc_carry[i].has_value()) {
+			pc_carry[i] = *tiles[y][x].o;
+			tiles[y][x].o = NULL;
+			return;
+		}
+	}
+}
+
+static void
+carry_list(WINDOW *const cwin, carry_action const action)
+{
+	std::optional<std::string> error;
+	do {
+		if (werase(cwin) == ERR) {
+			errx(1, "carry_list erase");
+		}
+
+		if (action == CARRY_REMOVE) {
+			wattron(cwin, COLOR_PAIR(COLOR_RED));
+		}
+
+		(void)box(cwin, 0, 0);
+
+		switch (action) {
+		case CARRY_DROP:
+			(void)mvwprintw(cwin, HEIGHT - 1, 2,
+				"[ 0-9 to drop, ESC to exit ]");
+			break;
+		case CARRY_INSPECT:
+			(void)mvwprintw(cwin, HEIGHT - 1, 2,
+				"[ 0-9 to inspect, ESC to exit ]");
+			break;
+		case CARRY_REMOVE:
+			(void)mvwprintw(cwin, HEIGHT - 1, 2,
+				"[ 0-9 to REMOVE, ESC to exit ]");
+			break;
+		case CARRY_LIST:
+			(void)mvwprintw(cwin, HEIGHT - 1, 2,
+				"[ press any key to exit ]");
+			break;
+		case CARRY_WEAR:
+			(void)mvwprintw(cwin, HEIGHT - 1, 2,
+				"[ 0-9 to equip, ESC to exit ]");
+			break;
+		}
+
+		if (error.has_value()) {
+			(void)mvwprintw(cwin, 0, 2, "[ error: %s ]",
+				error->c_str());
+			error.reset();
+		}
+
+		if (action == CARRY_REMOVE) {
+			wattroff(cwin, COLOR_PAIR(COLOR_RED));
+		}
+
+
+		for (int i = 0; i < PC_CARRY_MAX; ++i) {
+			if (pc_carry[i].has_value()) {
+				wattron(cwin, pc_carry[i]->color);
+				(void)mvwprintw(cwin, i + 5, 2,
+					"%d. %s: \t'%c'\t%s", i,
+					type_map_name[pc_carry[i]->obj_type],
+					pc_carry[i]->symb,
+					pc_carry[i]->name.c_str());
+				wattroff(cwin, pc_carry[i]->color);
+			} else {
+				(void)mvwprintw(cwin, i + 5, 2, "%u.", i);
+			}
+		}
+
+		int const ch = wgetch(cwin);
+
+		if (action == CARRY_LIST) {
+			return;
+		}
+
+		switch(ch) {
+		case ERR:
+			errx(1, "carry_list wgetch ERR");
+			return;
+		case KEY_ESC:
+			return;
+		case '0':
+		case '1':
+		case '2':
+		case '3':
+		case '4':
+		case '5':
+		case '6':
+		case '7':
+		case '8':
+		case '9':
+			int const i = ch - '0';
+
+			if (!pc_carry[i].has_value()) {
+				error = std::string("slot ") + std::to_string(i)
+					+ " has no item";
+				break;
+			}
+
+			if (action == CARRY_WEAR) {
+				if (!type_map_equip[pc_carry[i]->obj_type]) {
+					error = std::string("item in slot ")
+						+ std::to_string(i)
+						+ " cannot be eqipped";
+					break;
+				}
+
+				carry_to_equip(i);
+			} else if (action == CARRY_DROP) {
+				tiles[player.y][player.x].o = &(*pc_carry[i]);
+				pc_carry[i].reset();
+			} else if (action == CARRY_REMOVE) {
+				pc_carry[i].reset();
+			} else if (action == CARRY_INSPECT) {
+				thing_details(cwin, *pc_carry[i]);
+			}
+
+			break;
+		}
+	} while (1);
+}
+
+static void
+print_equipped(WINDOW *const ewin, int const i, char const *const name,
+	char const ch, std::optional<obj> const &item)
+{
+	if (item.has_value()) {
+		wattron(ewin, item->color);
+		(void)mvwprintw(ewin, i, 2, "%s\t%c.\t'%c'\t%s", name, ch,
+			item->symb, item->name.c_str());
+		wattroff(ewin, item->color);
+	} else {
+		(void)mvwprintw(ewin, i, 2, "%s\t%c.", name, ch);
+	}
+}
+
+static void
+equip_list(WINDOW *const ewin, bool const take)
+{
+	std::optional<std::string> error;
+	int const length = 12;
+	std::tuple<std::optional<obj> const *const, char const *const, char> const equip[] = {
+		{ &pc_equip.amulet,	"amulet",	'a' },
+		{ &pc_equip.armor,	"armor\t",	'b' },
+		{ &pc_equip.boots,	"boots\t",	'c' },
+		{ &pc_equip.cloak,	"cloak\t",	'd' },
+		{ &pc_equip.gloves,	"gloves",	'e' },
+		{ &pc_equip.helmet,	"helmet",	'f' },
+		{ &pc_equip.light,	"light\t",	'g' },
+		{ &pc_equip.offhand,	"offhand",	'h' },
+		{ &pc_equip.ranged,	"ranged",	'i' },
+		{ &pc_equip.ring_left,	"left ring",	'j' },
+		{ &pc_equip.ring_right,	"right ring",	'k' },
+		{ &pc_equip.weapon,	"weapon",	'l' }
+	};
+
+	do {
+		if (werase(ewin) == ERR) {
+			errx(1, "equip_list erase");
+		}
+
+		(void)box(ewin, 0, 0);
+
+		if (take) {
+			(void)mvwprintw(ewin, HEIGHT - 1, 2,
+				"[ a-l to take off, ESC to exit ]");
+		} else {
+			(void)mvwprintw(ewin, HEIGHT - 1, 2,
+				"[ press any key to exit ]");
+		}
+
+		if (error.has_value()) {
+			(void)mvwprintw(ewin, 0, 2, "[ error: %s ]",
+				error->c_str());
+			error.reset();
+		}
+
+		for (int i = 0; i < length; ++i) {
+			print_equipped(ewin, i + 4, std::get<1>(equip[i]),
+				std::get<2>(equip[i]), *std::get<0>(equip[i]));
+		}
+
+		int const ch = wgetch(ewin);
+
+		if (!take) {
+			return;
+		}
+
+		switch(ch) {
+		case ERR:
+			errx(1, "equip_list wgetch ERR");
+			return;
+		case KEY_ESC:
+			return;
+		default:
+			equip_to_carry(ch, error);
+			break;
+		}
+	} while (1);
+}
+
+static void
+carry_to_equip(int const i)
+{
+	std::optional<obj> *equip_slot;
+
+	switch(pc_carry[i]->obj_type) {
+	case amulet:
+		equip_slot = &pc_equip.amulet;
+		break;
+	case armor:
+		equip_slot = &pc_equip.armor;
+		break;
+	case boots:
+		equip_slot = &pc_equip.boots;
+		break;
+	case cloak:
+		equip_slot = &pc_equip.cloak;
+		break;
+	case gloves:
+		equip_slot = &pc_equip.gloves;
+		break;
+	case helmet:
+		equip_slot = &pc_equip.helmet;
+		break;
+	case light:
+		equip_slot = &pc_equip.light;
+		break;
+	case offhand:
+		equip_slot = &pc_equip.offhand;
+		break;
+	case ranged:
+		equip_slot = &pc_equip.ranged;
+		break;
+	case ring:
+		if (pc_equip.ring_right.has_value()) {
+			equip_slot = &pc_equip.ring_left;
+		} else {
+			equip_slot = &pc_equip.ring_right;
+		}
+		break;
+	case weapon:
+		equip_slot = &pc_equip.weapon;
+		break;
+	default:
+		errx(1, "carry_to_equip bad swap");
+	}
+
+	std::swap(pc_carry[i], *equip_slot);
+}
+
+static void
+equip_to_carry(int const i, std::optional<std::string> &error)
+{
+	std::optional<obj> *equip_slot;
+
+	switch(i) {
+	case 'a':
+		equip_slot = &pc_equip.amulet;
+		break;
+	case 'b':
+		equip_slot = &pc_equip.armor;
+		break;
+	case 'c':
+		equip_slot = &pc_equip.boots;
+		break;
+	case 'd':
+		equip_slot = &pc_equip.cloak;
+		break;
+	case 'e':
+		equip_slot = &pc_equip.gloves;
+		break;
+	case 'f':
+		equip_slot = &pc_equip.helmet;
+		break;
+	case 'g':
+		equip_slot = &pc_equip.light;
+		break;
+	case 'h':
+		equip_slot = &pc_equip.offhand;
+		break;
+	case 'i':
+		equip_slot = &pc_equip.ranged;
+		break;
+	case 'j':
+		equip_slot = &pc_equip.ring_left;
+		break;
+	case 'k':
+		equip_slot = &pc_equip.ring_right;
+		break;
+	case 'l':
+		equip_slot = &pc_equip.weapon;
+		break;
+	default:
+		return;
+	}
+
+	if (!equip_slot->has_value()) {
+		error = std::string("slot ") + (char)i + " has no item";
+	}
+
+	for (int j = 0; j < PC_CARRY_MAX; ++j) {
+		if (!pc_carry[j].has_value()) {
+			std::swap(pc_carry[j], *equip_slot);
+			return;
+		}
+	}
+
+	error = "no open slots in carry bag";
+}
+
+static void
+thing_details(WINDOW *const win, dungeon_thing const &d)
+{
+	std::stringstream ss(d.desc);
+	std::string tmp;
+
+	std::vector<std::string> lines;
+	std::vector<std::string>::size_type cpos = 0;
+
+	lines.push_back(std::string("Symbol: '") + (char)d.symb + "'\tName: "
+		+ d.name);
+	lines.push_back("");
+
+	while (std::getline(ss, tmp, '\n')) {
+		lines.push_back(tmp);
+	}
+
+	while (1) {
+		if (werase(win) == ERR) {
+			errx(1, "thing_details erase");
+		}
+
+		(void)box(win, 0, 0);
+
+		(void)mvwprintw(win, HEIGHT - 1, 2,
+			"[ arrow keys to scroll; ESC to exit ]");
+
+		std::size_t i;
+		for(i = 0; i < HEIGHT - 2 && i + cpos < lines.size(); ++i) {
+			(void)mvwprintw(win, static_cast<int>(i + 1U), 2,
+				lines[i + cpos].c_str());
+		}
+
+		for (; i < HEIGHT - 2; ++i) {
+			(void)mvwaddch(win, static_cast<int>(i + 1U), 2, '~');
+		}
+
+		if (wrefresh(win) == ERR) {
+			errx(1, "thing_details wrefresh");
+		}
+
+		switch(wgetch(win)) {
+		case ERR:
+			errx(1, "thing_details wgetch ERR");
+			return;
+		case KEY_UP:
+			if (--cpos > lines.size()) {
+				cpos = 0;
+			}
+			break;
+		case KEY_DOWN:
+			if (++cpos > lines.size() - 1) {
+				cpos = lines.size() - 1;
+			}
+			break;
+		case KEY_ESC:
+			return;
+		default:
+			break;
+		}
+	}
+}
diff --git a/turn.h b/turn.h
new file mode 100644
index 0000000..d727294
--- /dev/null
+++ b/turn.h
@@ -0,0 +1,33 @@
+/*
+ * OPAL's playable almost indefectibly.
+ * Copyright (C) 2019  Esote
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef TURN_H
+#define TURN_H
+
+#include <ncurses.h>
+
+enum turn_exit {
+	TURN_DEATH,
+	TURN_NEXT,
+	TURN_NONE,
+	TURN_QUIT,
+	TURN_WIN,
+};
+
+enum turn_exit	turn_engine(WINDOW *const, unsigned int const, unsigned int const);
+
+#endif /* TURN_H */