LambdaHack 0.1.20090606 → 0.1.20110117
raw patch · 67 files changed
+4920/−3605 lines, 67 filesdep +ConfigFiledep +MissingHdep +filepathdep ~basedep ~gtkdep ~mtl
Dependencies added: ConfigFile, MissingH, filepath, old-time
Dependency ranges changed: base, gtk, mtl, vty
Files
- Actor.hs +0/−29
- COPYING +0/−340
- CREDITS +4/−0
- DESIGN.markdown +200/−0
- Display.hs +0/−12
- Display/Curses.hs +0/−115
- Display/Gtk.hs +0/−159
- Display/Vty.hs +0/−50
- Display2.hs +0/−149
- Dungeon.hs +0/−254
- FOV.hs +0/−67
- File.hs +0/−22
- Frequency.hs +0/−31
- Geometry.hs +0/−60
- Item.hs +0/−234
- LICENSE +28/−0
- LambdaHack.cabal +28/−21
- LambdaHack.config.example +25/−0
- LambdaHack.hs +0/−61
- Level.hs +0/−512
- Message.hs +0/−28
- Monster.hs +0/−147
- PLAYING.markdown +111/−0
- Perception.hs +0/−48
- README +0/−12
- README.markdown +37/−0
- Random.hs +0/−67
- Save.hs +0/−34
- State.hs +0/−116
- Strategy.hs +0/−49
- Style.hs +0/−355
- Turn.hs +0/−621
- Version.hs +0/−12
- src/Action.hs +194/−0
- src/Actions.hs +593/−0
- src/Actor.hs +35/−0
- src/Command.hs +32/−0
- src/Config.hs +71/−0
- src/Display.hs +14/−0
- src/Display/Curses.hs +125/−0
- src/Display/Gtk.hs +214/−0
- src/Display/Vty.hs +64/−0
- src/Display2.hs +194/−0
- src/Dungeon.hs +324/−0
- src/FOV.hs +414/−0
- src/File.hs +22/−0
- src/Frequency.hs +31/−0
- src/Geometry.hs +71/−0
- src/Grammar.hs +56/−0
- src/HighScores.hs +147/−0
- src/Item.hs +255/−0
- src/ItemState.hs +16/−0
- src/Keybindings.hs +40/−0
- src/Keys.hs +32/−0
- src/LambdaHack.hs +79/−0
- src/Level.hs +545/−0
- src/LevelState.hs +33/−0
- src/Message.hs +30/−0
- src/Monster.hs +134/−0
- src/Perception.hs +85/−0
- src/Random.hs +74/−0
- src/Save.hs +36/−0
- src/State.hs +133/−0
- src/Strategy.hs +49/−0
- src/StrategyState.hs +72/−0
- src/Turn.hs +261/−0
- src/Version.hs +12/−0
− Actor.hs
@@ -1,29 +0,0 @@-module Actor where--import Level-import Monster--data Actor = AMonster Int -- offset in monster list- | APlayer- deriving (Show, Eq)--getActor :: Level -> Player -> Actor -> Monster-getActor lvl p (AMonster n) = lmonsters lvl !! n-getActor lvl p APlayer = p--updateActor :: (Monster -> Monster) -> -- the update- (Monster -> Level -> Player -> IO a) -> -- continuation- Actor -> -- who to update- Level -> Player -> IO a -- transformed continuation-updateActor f k (AMonster n) lvl p = - let (m,ms) = updateMonster f n (lmonsters lvl)- in k m (updateMonsters lvl (const ms)) p-updateActor f k APlayer lvl p = k p lvl (f p)--updateMonster :: (Monster -> Monster) -> Int -> [Monster] -> (Monster, [Monster])-updateMonster f n ms =- case splitAt n ms of- (pre, x : post) -> let m = f x in (m, pre ++ [m] ++ post)- xs -> error "updateMonster"--
− COPYING
@@ -1,340 +0,0 @@- GNU GENERAL PUBLIC LICENSE- Version 2, June 1991-- Copyright (C) 1989, 1991 Free Software Foundation, Inc.- 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA- Everyone is permitted to copy and distribute verbatim copies- of this license document, but changing it is not allowed.-- Preamble-- The licenses for most software are designed to take away your-freedom to share and change it. By contrast, the GNU General Public-License is intended to guarantee your freedom to share and change free-software--to make sure the software is free for all its users. This-General Public License applies to most of the Free Software-Foundation's software and to any other program whose authors commit to-using it. (Some other Free Software Foundation software is covered by-the GNU Library General Public License instead.) You can apply it to-your programs, too.-- When we speak of free software, we are referring to freedom, not-price. Our General Public Licenses are designed to make sure that you-have the freedom to distribute copies of free software (and charge for-this service if you wish), that you receive source code or can get it-if you want it, that you can change the software or use pieces of it-in new free programs; and that you know you can do these things.-- To protect your rights, we need to make restrictions that forbid-anyone to deny you these rights or to ask you to surrender the rights.-These restrictions translate to certain responsibilities for you if you-distribute copies of the software, or if you modify it.-- For example, if you distribute copies of such a program, whether-gratis or for a fee, you must give the recipients all the rights that-you have. You must make sure that they, too, receive or can get the-source code. And you must show them these terms so they know their-rights.-- We protect your rights with two steps: (1) copyright the software, and-(2) offer you this license which gives you legal permission to copy,-distribute and/or modify the software.-- Also, for each author's protection and ours, we want to make certain-that everyone understands that there is no warranty for this free-software. If the software is modified by someone else and passed on, we-want its recipients to know that what they have is not the original, so-that any problems introduced by others will not reflect on the original-authors' reputations.-- Finally, any free program is threatened constantly by software-patents. We wish to avoid the danger that redistributors of a free-program will individually obtain patent licenses, in effect making the-program proprietary. To prevent this, we have made it clear that any-patent must be licensed for everyone's free use or not licensed at all.-- The precise terms and conditions for copying, distribution and-modification follow.-- GNU GENERAL PUBLIC LICENSE- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION-- 0. This License applies to any program or other work which contains-a notice placed by the copyright holder saying it may be distributed-under the terms of this General Public License. The "Program", below,-refers to any such program or work, and a "work based on the Program"-means either the Program or any derivative work under copyright law:-that is to say, a work containing the Program or a portion of it,-either verbatim or with modifications and/or translated into another-language. (Hereinafter, translation is included without limitation in-the term "modification".) Each licensee is addressed as "you".--Activities other than copying, distribution and modification are not-covered by this License; they are outside its scope. The act of-running the Program is not restricted, and the output from the Program-is covered only if its contents constitute a work based on the-Program (independent of having been made by running the Program).-Whether that is true depends on what the Program does.-- 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the-notices that refer to this License and to the absence of any warranty;-and give any other recipients of the Program a copy of this License-along with the Program.--You may charge a fee for the physical act of transferring a copy, and-you may at your option offer warranty protection in exchange for a fee.-- 2. You may modify your copy or copies of the Program or any portion-of it, thus forming a work based on the Program, and copy and-distribute such modifications or work under the terms of Section 1-above, provided that you also meet all of these conditions:-- a) You must cause the modified files to carry prominent notices- stating that you changed the files and the date of any change.-- b) You must cause any work that you distribute or publish, that in- whole or in part contains or is derived from the Program or any- part thereof, to be licensed as a whole at no charge to all third- parties under the terms of this License.-- c) If the modified program normally reads commands interactively- when run, you must cause it, when started running for such- interactive use in the most ordinary way, to print or display an- announcement including an appropriate copyright notice and a- notice that there is no warranty (or else, saying that you provide- a warranty) and that users may redistribute the program under- these conditions, and telling the user how to view a copy of this- License. (Exception: if the Program itself is interactive but- does not normally print such an announcement, your work based on- the Program is not required to print an announcement.)--These requirements apply to the modified work as a whole. If-identifiable sections of that work are not derived from the Program,-and can be reasonably considered independent and separate works in-themselves, then this License, and its terms, do not apply to those-sections when you distribute them as separate works. But when you-distribute the same sections as part of a whole which is a work based-on the Program, the distribution of the whole must be on the terms of-this License, whose permissions for other licensees extend to the-entire whole, and thus to each and every part regardless of who wrote it.--Thus, it is not the intent of this section to claim rights or contest-your rights to work written entirely by you; rather, the intent is to-exercise the right to control the distribution of derivative or-collective works based on the Program.--In addition, mere aggregation of another work not based on the Program-with the Program (or with a work based on the Program) on a volume of-a storage or distribution medium does not bring the other work under-the scope of this License.-- 3. You may copy and distribute the Program (or a work based on it,-under Section 2) in object code or executable form under the terms of-Sections 1 and 2 above provided that you also do one of the following:-- a) Accompany it with the complete corresponding machine-readable- source code, which must be distributed under the terms of Sections- 1 and 2 above on a medium customarily used for software interchange; or,-- b) Accompany it with a written offer, valid for at least three- years, to give any third party, for a charge no more than your- cost of physically performing source distribution, a complete- machine-readable copy of the corresponding source code, to be- distributed under the terms of Sections 1 and 2 above on a medium- customarily used for software interchange; or,-- c) Accompany it with the information you received as to the offer- to distribute corresponding source code. (This alternative is- allowed only for noncommercial distribution and only if you- received the program in object code or executable form with such- an offer, in accord with Subsection b above.)--The source code for a work means the preferred form of the work for-making modifications to it. For an executable work, complete source-code means all the source code for all modules it contains, plus any-associated interface definition files, plus the scripts used to-control compilation and installation of the executable. However, as a-special exception, the source code distributed need not include-anything that is normally distributed (in either source or binary-form) with the major components (compiler, kernel, and so on) of the-operating system on which the executable runs, unless that component-itself accompanies the executable.--If distribution of executable or object code is made by offering-access to copy from a designated place, then offering equivalent-access to copy the source code from the same place counts as-distribution of the source code, even though third parties are not-compelled to copy the source along with the object code.-- 4. You may not copy, modify, sublicense, or distribute the Program-except as expressly provided under this License. Any attempt-otherwise to copy, modify, sublicense or distribute the Program is-void, and will automatically terminate your rights under this License.-However, parties who have received copies, or rights, from you under-this License will not have their licenses terminated so long as such-parties remain in full compliance.-- 5. You are not required to accept this License, since you have not-signed it. However, nothing else grants you permission to modify or-distribute the Program or its derivative works. These actions are-prohibited by law if you do not accept this License. Therefore, by-modifying or distributing the Program (or any work based on the-Program), you indicate your acceptance of this License to do so, and-all its terms and conditions for copying, distributing or modifying-the Program or works based on it.-- 6. Each time you redistribute the Program (or any work based on the-Program), the recipient automatically receives a license from the-original licensor to copy, distribute or modify the Program subject to-these terms and conditions. You may not impose any further-restrictions on the recipients' exercise of the rights granted herein.-You are not responsible for enforcing compliance by third parties to-this License.-- 7. If, as a consequence of a court judgment or allegation of patent-infringement or for any other reason (not limited to patent issues),-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-distribute so as to satisfy simultaneously your obligations under this-License and any other pertinent obligations, then as a consequence you-may not distribute the Program at all. For example, if a patent-license would not permit royalty-free redistribution of the Program by-all those who receive copies directly or indirectly through you, then-the only way you could satisfy both it and this License would be to-refrain entirely from distribution of the Program.--If any portion of this section is held invalid or unenforceable under-any particular circumstance, the balance of the section is intended to-apply and the section as a whole is intended to apply in other-circumstances.--It is not the purpose of this section to induce you to infringe any-patents or other property right claims or to contest validity of any-such claims; this section has the sole purpose of protecting the-integrity of the free software distribution system, which is-implemented by public license practices. Many people have made-generous contributions to the wide range of software distributed-through that system in reliance on consistent application of that-system; it is up to the author/donor to decide if he or she is willing-to distribute software through any other system and a licensee cannot-impose that choice.--This section is intended to make thoroughly clear what is believed to-be a consequence of the rest of this License.-- 8. If the distribution and/or use of the Program is restricted in-certain countries either by patents or by copyrighted interfaces, the-original copyright holder who places the Program under this License-may add an explicit geographical distribution limitation excluding-those countries, so that distribution is permitted only in or among-countries not thus excluded. In such case, this License incorporates-the limitation as if written in the body of this License.-- 9. The Free Software Foundation may publish revised and/or new versions-of the 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 a version number of this License which applies to it and "any-later version", you have the option of following the terms and conditions-either of that version or of any later version published by the Free-Software Foundation. If the Program does not specify a version number of-this License, you may choose any version ever published by the Free Software-Foundation.-- 10. If you wish to incorporate parts of the Program into other free-programs whose distribution conditions are different, write to the author-to ask for permission. For software which is copyrighted by the Free-Software Foundation, write to the Free Software Foundation; we sometimes-make exceptions for this. Our decision will be guided by the two goals-of preserving the free status of all derivatives of our free software and-of promoting the sharing and reuse of software generally.-- NO WARRANTY-- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.-- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR-REDISTRIBUTE 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.-- 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-convey the exclusion of warranty; and each file should have at least-the "copyright" line and a pointer to where the full notice is found.-- <one line to give the program's name and a brief idea of what it does.>- Copyright (C) <year> <name of author>-- This program is free software; you can redistribute it and/or modify- it under the terms of the GNU General Public License as published by- the Free Software Foundation; either version 2 of the License, or- (at your option) any later version.-- This program is distributed in the hope that it will be useful,- but WITHOUT ANY WARRANTY; without even the implied warranty of- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the- GNU General Public License for more details.-- You should have received a copy of the GNU General Public License- along with this program; if not, write to the Free Software- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA---Also add information on how to contact you by electronic and paper mail.--If the program is interactive, make it output a short notice like this-when it starts in an interactive mode:-- Gnomovision version 69, Copyright (C) year name of author- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.- This is free software, and you are welcome to redistribute it- under certain conditions; type `show c' for details.--The hypothetical commands `show w' and `show c' should show the appropriate-parts of the General Public License. Of course, the commands you use may-be called something other than `show w' and `show c'; they could even be-mouse-clicks or menu items--whatever suits your program.--You should also get your employer (if you work as a programmer) or your-school, if any, to sign a "copyright disclaimer" for the program, if-necessary. Here is a sample; alter the names:-- Yoyodyne, Inc., hereby disclaims all copyright interest in the program- `Gnomovision' (which makes passes at compilers) written by James Hacker.-- <signature of Ty Coon>, 1 April 1989- Ty Coon, President of Vice--This General Public License does not permit incorporating your program into-proprietary programs. If your program is a subroutine library, you may-consider it more useful to permit linking proprietary applications with the-library. If this is what you want to do, use the GNU Library General-Public License instead of this License.
+ CREDITS view
@@ -0,0 +1,4 @@+Developers who have contributed significantly to LambdaHack:++Andres Loeh+Mikolaj Konarski
+ DESIGN.markdown view
@@ -0,0 +1,200 @@+Design notes+============++Here is a rationale for some design decisions and implementation details.+Not all of the sketched features are implemented and not everything+is implemented as described.+++Level generation+----------------++Each level is generated by an algorithm inspired by the original Rogue,+as follows:++ * The available area is divided into a 3 by 3 grid+ where each of the 9 grid cells has approximately the same size.++ * In each of the 9 grid cells one room is placed at a random location.+ The minimum size of a room is 2 by 2 floor tiles. A room is surrounded+ by walls, and the walls still have to fit into the assigned grid cells.++ * Rooms that are on horizontally or vertically adjacent grid cells+ may be connected by a corridor. Corridors consist of 3 segments of straight+ lines (either "horizontal, vertical, horizontal" or "vertical, horizontal,+ vertical"). They end in openings in the walls of the room they connect.+ It is possible that one or two of the 3 segments have length 0, such that+ the resulting corridor is L-shaped or even a single straight line.++ * Corridors are generated randomly in such a way that at least every room+ on the grid is connected, and a few more might be. It is not sufficient+ to always connect all adjacent rooms.++ * Stairs up and down are placed. Stairs are always located in two different+ randomly chosen rooms.+++Field Of View+-------------++The algorithm used is a variant of Shadow Casting. We first compute+fields that are reachable (have unobstructed line of sight) from the hero's+position. Later, from this information we compute the fields that+are visible (not hidden in darkness, etc.).++As input to the algorithm, we require information about fields that+block light. As output, we get information on the reachability of all fields.+We assume that the hero is located at position (0, 0)+and we only consider fields (line, row) where line >= 0 and 0 <= row <= line.+This is just about one eighth of the whole hero's surroundings,+but the other parts can be computed in the same fashion by mirroring+or rotating the given algorithm accordingly.++ fov (blocks, maxline) =+ shadow := \empty_set+ reachable (0, 0) := True+ for l \in [ 1 .. maxline ] do+ for r \in [ 0 .. l ] do+ reachable (l, r) := ( \exists a. a \in interval (l, r) \and+ a \not_in shadow)+ if blocks (l, r) then+ shadow := shadow \union interval (l, r)+ end if+ end for+ end for+ return reachable++ interval (l, r) = return [ angle (l + 0.5, r - 0.5),+ angle (l - 0.5, r + 0.5) ]+ angle (l, r) = return atan (r / l)++The algorithm traverses the fields line by line, row by row.+At every moment, we keep in shadow the intervals which are in shadow,+measured by their angle. A square is reachable when any point+in it is not in shadow --- the algorithm is permissive in this respect.+We could also require that a certain fraction of the field is reachable,+or a specific point. Our choice has certain consequences. For instance,+a single blocking field throws a shadow, but the fields immediately behind+the blocking field are still visible.++We can compute the interval of angles corresponding to one square field+by computing the angle of the line passing the upper left corner+and the angle of the line passing the lower right corner.+This is what interval and angle do. If a field is blocking, the interval+for the square is added to the shadow set.++Once we compute the reachable fields using FOV, it is possible+to compute what the hero can actually see. Fields adjacent to the hero+(also diagonally) can always be seen (except for walls).+Fields that have light and are reachable can also be seen.+We treat floor of rooms as having light, whereas corridors and rock are dark.++Walls reflect light. They can be seen only if an adjacent floor field+can also be seen. In particular, walls cannot be seen when passing+a corridor on the outside of a room, but can be seen from the inside of a room.+++Monster movement+----------------++Not all monsters use the same algorithm to find the hero.+Some implemented and unimplemented methods are listed below:++* Random+The simplest way to have a monster move is at random.++* Sight+If a monster can see the hero (as an approximation,+we assume it is the case when the hero can see the monster),+the monster should move toward the hero.++* Smell+The hero leaves a trail when moving toward the dungeon.+For a certain timespan (100--200 moves), it is possible+for certain monsters to detect that a hero has been at a certain field.+Once a monster is following a trail, it should move to the+neighboring field where the hero has most recently visited.++* Noise+The hero makes noise. If the distance between the hero+and the monster is small enough, the monster can hear the hero+and moves into the approximate direction of the hero.+++Dungeon tiles+-------------++Abstract musings for now; not implemented.++The hero and the monsters (later, in short, 'monsters') can transform a tile,+which can be represented by a graph, with edges labeled by prerequisites+and cost of transformation. Monsters can also melee across a tile border,+and it's always permitted (e.g. fighting a ghost embedded in a wall)+and the kind of the tiles involved is irrelevant.++For tile design, I disregard sound and the monster's sense of hearing,+because sound is best conveyed to the player through sound effects,+not by painting tiles, and this requires lots of work.+Acoustics is quite complex, too. Right now, sound ignores tiles+and sound cues are given as text messages, e.g., when a monster attacks+or is hit or when distant (but not too distant?) monsters fight or when+a level is eerie silent, when the hero enters.++Monsters can interact directly and non-destructively with dungeon tiles+in the following ways: they can move trough, see through, shoot through+and smell (or inhale) across.++Three different kinds of things can pass through a tile:++ * objects: big, slow, pushy things (monsters passing through tiles+ and throwing objects from inventory across tiles)++ * projectiles and gases: monsters shooting small, fast and sharp things+ (arrows and bolts from the quiver) and monsters inhaling tiny, slow+ particles (smells, smoke, fog, poisonous gases)++ * light: monsters seeing clearly across a tile (light that just leaks+ through a cloth or produces a distorted image though a waterfall+ does not count)++For simplicity I assume that if big objects can move through,+small objects can as well (no Kevlar curtains nor automatic doors).+Also, I merged projectiles and gases, assuming that if small objects+can get through, so can tiny objects (no self-sealing rubber walls)+and the reverse (no vents in walls).++I can find no such simplifications for light. I only assume that the light+that carries the picture is in itself too weak to illuminate any tile+(so you can stand in a pitch dark corridor and observe a nearby sunny room).+Consequently, room lighting and monster field of view calculations+are very loosely coupled.++Below are tables with examples of different tile kinds.++The case of tiles that can be shot through and smelled through:++ can see through cannot see through++ can pass floor, open door curtain, waterfall++ cannot pass fence, grate grate with waterfall++The case of tiles that cannot be shot through and that block smell:++ can see through cannot see through++ can pass none none++ cannot pass crystal, glass rock, closed door++Note that acid pools and pits do not count as "cannot pass" tiles.+First, axes and rocks can be thrown across (or into) them.+Second, the hero and monsters can be pushed into them (and perish).+The player cannot steer the hero into the acid pool not by physical+impossibility, but by the self preservation instinct of the hero.+So, an acid pool is in the same category as empty floor and it's up to the+monster AI routines to check if the monster has wings (and a brain of any size)+before entering. Such tiles should probably be marked as "damage this large+unless flying". Similarly with water and swimming, lava pool and fire+resistance, poison cloud and poison resistance, etc. No action is forbidden+there, but each action has consequences.
− Display.hs
@@ -1,12 +0,0 @@-module Display (module D) where---- wrapper for selected Display frontend--#ifdef CURSES-import Display.Curses as D-#elif GTK-import Display.Gtk as D-#else-import Display.Vty as D-#endif-
− Display/Curses.hs
@@ -1,115 +0,0 @@-module Display.Curses- (displayId, startup, shutdown,- display, nextEvent, setBG, setFG, setBold, Session,- white, black, yellow, blue, magenta, red, green, attr, Display.Curses.Attr) where--import UI.HSCurses.Curses as C hiding (setBold)-import qualified UI.HSCurses.CursesHelper as C-import Data.List as L-import Data.Map as M-import Data.Char-import qualified Data.ByteString as BS--import Geometry--displayId = "curses"--data Session =- Session- { win :: Window,- styles :: Map (Maybe AttrColor, Maybe AttrColor) C.CursesStyle }--startup :: (Session -> IO ()) -> IO ()-startup k =- do- C.start- C.startColor- cursSet CursorInvisible- nr <- colorPairs- let s = [ ((f,b), C.Style (toFColor f) (toBColor b))- | f <- Nothing : L.map Just [minBound..maxBound],- b <- Nothing : L.map Just [minBound..maxBound] ]- let (ks, vs) = unzip (tail s) -- drop the Nothing/Nothing combo- ws <- C.convertStyles (take (nr - 1) vs)- k (Session C.stdScr (M.fromList (zip ks ws)))--shutdown :: Session -> IO ()-shutdown w = C.end--display :: Area -> Session -> (Loc -> (Display.Curses.Attr, Char)) -> String -> String -> IO ()-display ((y0,x0),(y1,x1)) (Session { win = w, styles = s }) f msg status =- do- erase- mvWAddStr w 0 0 msg- sequence_ [ let (a,c) = f (y,x) in C.setStyle (findWithDefault C.defaultCursesStyle a s) >> mvWAddStr w (y+1) x [c]- | x <- [x0..x1], y <- [y0..y1] ]- mvWAddStr w (y1+2) 0 status- refresh-{-- in V.update vty (Pic NoCursor - ((renderBS attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1-x0+1) msg)))) <->- img <-> - (renderBS attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1-x0+1) status))))))--}--{--toWidth :: Int -> String -> String-toWidth n x = take n (x ++ repeat ' ')--}--nextEvent :: Session -> IO String-nextEvent session =- do- e <- C.getKey refresh- case e of- C.KeyChar '<' -> return "less"- C.KeyChar '>' -> return "greater"- C.KeyChar '.' -> return "period"- C.KeyChar ':' -> return "colon"- C.KeyChar ',' -> return "comma"- C.KeyChar ' ' -> return "space"- C.KeyChar '?' -> return "question"- C.KeyChar '*' -> return "asterisk"- C.KeyChar '\ESC' -> return "Escape"- C.KeyChar c -> return [c]- C.KeyExit -> return "Escape"- _ -> nextEvent session--type Attr = (Maybe AttrColor, Maybe AttrColor)--attr = (Nothing, Nothing)--data AttrColor = White | Black | Yellow | Blue | Magenta | Red | Green - deriving (Show, Eq, Ord, Enum, Bounded)--toFColor :: Maybe AttrColor -> C.ForegroundColor-toFColor (Just White) = C.WhiteF-toFColor (Just Black) = C.BlackF-toFColor (Just Yellow) = C.BrownF-toFColor (Just Blue) = C.DarkBlueF-toFColor (Just Magenta) = C.PurpleF-toFColor (Just Red) = C.DarkRedF-toFColor (Just Green) = C.DarkGreenF-toFColor Nothing = C.DefaultF--toBColor :: Maybe AttrColor -> C.BackgroundColor-toBColor (Just White) = C.WhiteB-toBColor (Just Black) = C.BlackB-toBColor (Just Yellow) = C.BrownB-toBColor (Just Blue) = C.DarkBlueB-toBColor (Just Magenta) = C.PurpleB-toBColor (Just Red) = C.DarkRedB-toBColor (Just Green) = C.DarkGreenB-toBColor Nothing = C.DefaultB--white = White-black = Black-yellow = Yellow-blue = Blue-magenta = Magenta-red = Red-green = Green--setBold (f, b) = (f, b)-setFG c (_, b) = (Just c, b)-setBG c (f, _) = (f, Just c)
− Display/Gtk.hs
@@ -1,159 +0,0 @@-module Display.Gtk- (displayId, startup, shutdown, - display, nextEvent, setBG, setFG, setBold, Session,- white, black, yellow, blue, magenta, red, green, attr, Attr) where--import Control.Monad-import Control.Concurrent-import Graphics.UI.Gtk hiding (Attr)-import Data.List as L-import Data.IORef-import Data.Map as M--import Geometry--displayId = "gtk"--data Session =- Session {- schan :: Chan String,- stags :: Map AttrKey TextTag,- sview :: TextView }--doAttr :: TextTag -> AttrKey -> IO ()-doAttr tt (BG Blue) = set tt [ textTagBackground := "#0000CC" ]-doAttr tt (BG Magenta) = set tt [ textTagBackground := "#CC00CC" ]-doAttr tt (BG Green) = set tt [ textTagBackground := "#00CC00" ]-doAttr tt (BG Red) = set tt [ textTagBackground := "#CC0000" ]-doAttr tt (BG White) = set tt [ textTagBackground := "#FFFFFF" ]-doAttr tt (FG Green) = set tt [ textTagForeground := "#00FF00" ]-doAttr tt (FG Red) = set tt [ textTagForeground := "#FF0000" ]-doAttr tt (FG Blue) = set tt [ textTagForeground := "#0000FF" ]-doAttr tt (FG Yellow) = set tt [ textTagForeground := "#CCCC00" ]-doAttr tt (FG Black) = set tt [ textTagForeground := "#000000" ]-doAttr tt _ = return ()--startup :: (Session -> IO ()) -> IO ()-startup k =- do- initGUI- w <- windowNew-- ttt <- textTagTableNew- -- text attributes- tts <- fmap M.fromList $- mapM (\ c -> do- tt <- textTagNew Nothing- textTagTableAdd ttt tt- doAttr tt c- return (c,tt))- [ x | c <- [minBound .. maxBound], x <- [FG c, BG c]]-- -- text buffer- tb <- textBufferNew (Just ttt)- textBufferSetText tb (unlines (replicate 25 (replicate 80 ' ')))-- -- create text view- tv <- textViewNewWithBuffer tb- containerAdd w tv- textViewSetEditable tv False- textViewSetCursorVisible tv False-- -- font- f <- fontDescriptionNew- fontDescriptionSetFamily f "Monospace"- widgetModifyFont tv (Just f)- currentfont <- newIORef f- onButtonPress tv (\ e -> case e of- Button { eventButton = RightButton } ->- do- fsd <- fontSelectionDialogNew "Choose font"- cf <- readIORef currentfont- -- fd <- fontDescriptionToString cf- -- fontSelectionDialogSetFontName fsd fd- fontSelectionDialogSetPreviewText fsd "+##@##-...|"- response <- dialogRun fsd- when (response == ResponseOk) $- do- fn <- fontSelectionDialogGetFontName fsd- case fn of- Just fn' -> do- fd <- fontDescriptionFromString fn'- writeIORef currentfont fd- widgetModifyFont tv (Just fd)- Nothing -> return ()- widgetDestroy fsd- return True- _ -> return False)-- let black = Color minBound minBound minBound- let white = Color maxBound maxBound maxBound- widgetModifyBase tv StateNormal black- widgetModifyText tv StateNormal white-- ec <- newChan - forkIO $ k (Session ec tts tv)- - onKeyPress tv (\ e -> writeChan ec (eventKeyName e) >> yield >> return True)-- idleAdd (yield >> return True) priorityDefaultIdle- onDestroy w mainQuit -- set quit handler- widgetShowAll w- yield- mainGUI--shutdown _ = mainQuit--display :: Area -> Session -> (Loc -> (Attr, Char)) -> String -> String -> IO ()-display ((y0,x0),(y1,x1)) session f msg status =- do- sbuf <- textViewGetBuffer (sview session)- ttt <- textBufferGetTagTable sbuf- tb <- textBufferNew (Just ttt)- let text = unlines [ [ snd (f (y,x)) | x <- [x0..x1] ] | y <- [y0..y1] ]- textBufferSetText tb (msg ++ "\n" ++ text ++ status)- sequence_ [ setTo tb (stags session) (y,x) a | - y <- [y0..y1], x <- [x0..x1], let loc = (y,x), let (a,c) = f (y,x) ]- textViewSetBuffer (sview session) tb--setTo :: TextBuffer -> Map AttrKey TextTag -> Loc -> Attr -> IO ()-setTo tb tts (ly,lx) a =- do- ib <- textBufferGetIterAtLineOffset tb (ly+1) lx- ie <- textIterCopy ib- textIterForwardChar ie- mapM_ (\ c -> textBufferApplyTag tb (tts ! c) ib ie) a--nextEvent :: Session -> IO String-nextEvent = readChan . schan--setBold = id -- not supported yet-setBG c = (BG c :)-setFG c = (FG c :)-blue = Blue-magenta = Magenta-red = Red-yellow = Yellow-green = Green-white = White-black = Black-attr = []--type Attr = [AttrKey]--data AttrKey =- FG AttrColor- | BG AttrColor- deriving (Eq, Ord)--type Color = AttrColor--data AttrColor =- Blue- | Magenta- | Red- | Green- | Yellow- | White- | Black- deriving (Eq, Ord, Enum, Bounded)
− Display/Vty.hs
@@ -1,50 +0,0 @@-module Display.Vty- (displayId, startup, shutdown,- display, nextEvent, setBold, setBG, setFG, Session,- white, black, yellow, blue, magenta, red, green, attr, Attr) where--import Graphics.Vty as V-import Data.List as L-import Data.Char-import qualified Data.ByteString as BS--import Geometry--displayId = "vty"--type Session = V.Vty--startup :: (Session -> IO ()) -> IO ()-startup k = V.mkVty >>= k--display :: Area -> Session -> (Loc -> (Attr, Char)) -> String -> String -> IO ()-display ((y0,x0),(y1,x1)) vty f msg status =- let img = (foldr (<->) V.empty . - L.map (foldr (<|>) V.empty . - L.map (\ (x,y) -> let (a,c) = f (y,x) in renderChar a c)))- [ [ (x,y) | x <- [x0..x1] ] | y <- [y0..y1] ]- in V.update vty (Pic NoCursor - (renderBS attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1 - x0 + 1) msg))) <->- img <-> - renderBS attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1 - x0 + 1) status)))))--toWidth :: Int -> String -> String-toWidth n x = take n (x ++ repeat ' ')--nextEvent :: Session -> IO String-nextEvent session =- do- e <- V.getEvent session- case e of- V.EvKey (KASCII '<') [] -> return "less"- V.EvKey (KASCII '>') [] -> return "greater"- V.EvKey (KASCII '.') [] -> return "period"- V.EvKey (KASCII ':') [] -> return "colon"- V.EvKey (KASCII ',') [] -> return "comma"- V.EvKey (KASCII ' ') [] -> return "space"- V.EvKey (KASCII '?') [] -> return "question"- V.EvKey (KASCII '*') [] -> return "asterisk"- V.EvKey (KASCII c) [] -> return [c]- V.EvKey KEsc [] -> return "Escape"- V.EvKey KEnter [] -> return "Return"- _ -> nextEvent session
− Display2.hs
@@ -1,149 +0,0 @@-module Display2 (module Display, module Display2) where--import Data.Set as S-import Data.List as L-import Data.Map as M--import Message-import Display-import State-import Geometry-import Level-import Perception-import Monster-import Item---- | Displays a message on a blank screen. Waits for confirmation.-displayBlankConfirm :: Session -> String -> IO Bool-displayBlankConfirm session txt =- let x = txt ++ more- in do- display ((0,0),(0,length x - 1)) session (const (attr, ' ')) x ""- getConfirm session---- | Waits for a space or return.-getConfirm :: Session -> IO Bool-getConfirm session =- getOptionalConfirm session return (const $ getConfirm session)--getOptionalConfirm :: Session -> (Bool -> IO a) -> (String -> IO a) -> IO a-getOptionalConfirm session h k =- do- e <- nextEvent session- handleModifier e (getOptionalConfirm session h k) $- case e of- "space" -> h True- "Return" -> h True- "Escape" -> h False- _ -> k e---- | Handler that ignores modifier events as they are--- currently produced by the Gtk frontend.-handleModifier :: String -> IO a -> IO a -> IO a-handleModifier e h k =- case e of- "Shift_R" -> h- "Shift_L" -> h- "Control_L" -> h- "Control_R" -> h- "Super_L" -> h- "Super_R" -> h- "Menu" -> h- "Alt_L" -> h- "Alt_R" -> h- _ -> k---- | Configurable event handler for the direction keys. Is used to--- handle player moves, but can also be used for directed commands--- such as open/close.-handleDirection :: String -> ((Y,X) -> IO ()) -> IO () -> IO ()-handleDirection e h k =- case e of- "k" -> h (-1,0)- "j" -> h (1,0)- "h" -> h (0,-1)- "l" -> h (0,1)- "y" -> h (-1,-1)- "u" -> h (-1,1)- "b" -> h (1,-1)- "n" -> h (1,1)- _ -> k--splitOverlay :: Int -> String -> [[String]]-splitOverlay s xs = splitOverlay' (lines xs)- where- splitOverlay' ls- | length ls <= s = [ls] -- everything fits on one screen- | otherwise = let (pre,post) = splitAt (s - 1) ls- in (pre ++ [more]) : splitOverlay' post---- | Returns a function that looks up the characters in the--- string by location. Takes the height of the display plus--- the string. Returns also the number of screens required--- to display all of the string.-stringByLocation :: Y -> String -> (Int, Loc -> Maybe Char)-stringByLocation sy xs =- let- ls = splitOverlay sy xs- m = M.fromList (zip [0..] (L.map (M.fromList . zip [0..]) (concat ls)))- k = length ls- in- (k, \ (y,x) -> M.lookup y m >>= \ n -> M.lookup x n)--displayLevel :: Session -> Level -> Perception -> State -> Message -> IO ()-displayLevel session lvl per state msg = displayOverlay session lvl per state msg "" >> return ()--displayOverlay :: Session -> Level -> Perception -> State -> Message -> String -> IO Bool-displayOverlay session (lvl@(Level nm sz@(sy,sx) ms smap nlmap lmeta))- per- (state@(State { splayer = player@(Monster { mhp = php, mdir = pdir, mloc = ploc }), stime = time, sassocs = assocs }))- msg overlay =- let- reachable = preachable per- visible = pvisible per- sSml = ssensory state == Smell- sVis = ssensory state == Vision- sOmn = sdisplay state == Omniscient- sTer = case sdisplay state of Terrain n -> n; _ -> 0- lAt = if sOmn || sTer > 0 then at else rememberAt- lVision = if sVis- then \ vis rea ->- if vis then setBG blue- else if rea then setBG magenta- else id- else \ vis rea -> id- (n,over) = stringByLocation (sy+1) overlay -- n is the number of overlay screens- gold = maybe 0 (icount . fst) $ findItem (\ i -> iletter i == Just '$') (mitems player)- disp n msg = - display ((0,0),sz) session - (\ loc -> let tile = nlmap `lAt` loc- sml = ((smap ! loc) - time) `div` 100- vis = S.member loc visible- rea = S.member loc reachable- (rv,ra) = case L.find (\ m -> loc == mloc m) (player:ms) of- _ | sTer > 0 -> viewTerrain sTer False (tterrain tile)- Just m | sOmn || vis -> viewMonster (mtype m) - _ | sSml && sml >= 0 -> viewSmell sml- | otherwise -> viewTile vis tile assocs- vision = lVision vis rea- in- case over (loc `shift` ((sy+1) * n, 0)) of- Just c -> (attr, c)- _ -> (ra . vision $ attr, rv))- msg- (take 40 (levelName nm ++ repeat ' ') ++- take 10 ("$: " ++ show gold ++ repeat ' ') ++- take 10 ("HP: " ++ show php ++ repeat ' ') ++- take 10 ("T: " ++ show (time `div` 10) ++ repeat ' '))- msgs = splitMsg sx msg- perf k [] = perfo k ""- perf k [xs] = perfo k xs- perf k (x:xs) = disp n (x ++ more) >> getConfirm session >>= \ b ->- if b then perf k xs else return False- perfo k xs- | k < n - 1 = disp k xs >> getConfirm session >>= \ b ->- if b then perfo (k+1) xs else return False- | otherwise = disp k xs >> return True- in perf 0 msgs--
− Dungeon.hs
@@ -1,254 +0,0 @@-module Dungeon where--import Prelude hiding (floor)-import Control.Monad--import Data.Map as M-import Data.List as L-import Data.Ratio--import State-import Geometry-import Level-import Monster-import Item-import Random--type Corridor = [(Y,X)]-type Room = Area--mkRoom :: Int -> {- border columns -}- (Y,X) -> {- minimum size -}- Area -> {- this is an area, not the room itself -}- Rnd Room {- this is the upper-left and lower-right corner of the room -}-mkRoom bd (ym,xm)((y0,x0),(y1,x1)) =- do- (ry0,rx0) <- locInArea ((y0 + bd,x0 + bd),(y1 - bd - ym + 1,x1 - bd - xm + 1))- (ry1,rx1) <- locInArea ((ry0 + ym - 1,rx0 + xm - 1),(y1 - bd,x1 - bd))- return ((ry0,rx0),(ry1,rx1))--mkNoRoom :: Int -> {- border columns -}- Area -> {- this is an area, not the room itself -}- Rnd Room {- this is the upper-left and lower-right corner of the room -}-mkNoRoom bd ((y0,x0),(y1,x1)) =- do- (ry,rx) <- locInArea ((y0 + bd,x0 + bd),(y1 - bd,x1 - bd))- return ((ry,rx),(ry,rx))--mkCorridor :: HV -> (Loc,Loc) -> Area -> Rnd [(Y,X)] {- straight sections of the corridor -}-mkCorridor hv ((y0,x0),(y1,x1)) b =- do- (ry,rx) <- findLocInArea b (const True)- -- (ry,rx) is intermediate point the path crosses- -- hv decides whether we start in horizontal or vertical direction- case hv of- Horiz -> return [(y0,x0),(y0,rx),(y1,rx),(y1,x1)]- Vert -> return [(y0,x0),(ry,x0),(ry,x1),(y1,x1)]---- the condition passed to mkCorridor is tricky; there might not always--- exist a suitable intermediate point is the rooms are allowed to be close--- together ...-connectRooms :: Area -> Area -> Rnd [Loc]-connectRooms sa@((sy0,sx0),(sy1,sx1)) ta@((ty0,tx0),(ty1,tx1)) =- do- (sy,sx) <- locInArea sa- (ty,tx) <- locInArea ta- let xok = sx1 < tx0 - 3- let xarea = normalizeArea ((sy,sx1+2),(ty,tx0-2))- let yok = sy1 < ty0 - 3- let yarea = normalizeArea ((sy1+2,sx),(ty0-2,tx))- let xyarea = normalizeArea ((sy1+2,sx1+2),(ty0-2,tx0-2))- (hv,area) <- if xok && yok then fmap (\ hv -> (hv,xyarea)) (binaryChoice Horiz Vert)- else if xok then return (Horiz,xarea)- else return (Vert,yarea)- mkCorridor hv ((sy,sx),(ty,tx)) area--digCorridor :: Corridor -> LMap -> LMap-digCorridor (p1:p2:ps) l =- digCorridor (p2:ps) - (M.unionWith corridorUpdate (M.fromList [ (ps,newTile Corridor) | ps <- fromTo p1 p2 ]) l)- where- corridorUpdate _ (Tile (Wall hv) is,u) = (Tile (Opening hv) is,u)- corridorUpdate _ (Tile (Opening hv) is,u) = (Tile (Opening hv) is,u)- corridorUpdate _ (Tile (Floor l) is,u) = (Tile (Floor l) is,u)- corridorUpdate (x,u) _ = (x,u)-digCorridor _ l = l- -newTile :: Terrain -> (Tile, Tile)-newTile t = (Tile t [], Tile Unknown [])--bigroom :: LevelConfig -> - LevelName -> Rnd (Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) -> Level, Loc, Loc)-bigroom (LevelConfig { levelSize = (sy,sx) }) nm =- do- let lmap = digRoom Light ((1,1),(sy-1,sx-1)) (emptyLMap (sy,sx))- let smap = M.fromList [ ((y,x),-100) | y <- [0..sy], x <- [0..sx] ]- let lvl = Level nm (sy,sx) [] smap lmap ""- -- locations of the stairs- su <- findLoc lvl (const floor)- sd <- findLoc lvl (\ l t -> floor t && distance (su,l) > 676)- return (\ lu ld ->- let flmap = maybe id (\ l -> M.insert su (newTile (Stairs Light Up l))) lu $- maybe id (\ l -> M.insert sd (newTile (Stairs Light Down l))) ld- lmap- in Level nm (sy,sx) [] smap flmap "bigroom", su, sd)--data LevelConfig =- LevelConfig {- levelGrid :: Rnd (Y,X),- minRoomSize :: Rnd (Y,X),- darkRoomChance :: Rnd Bool,- border :: Int, -- must be at least 2!- levelSize :: (Y,X), -- lower right point- extraConnects :: (Y,X) -> Int, - -- relative to grid- -- (in fact a range, because of duplicate connects)- noRooms :: (Y,X) -> Rnd Int,- -- range, relative to grid- minStairsDistance :: Int, -- must not be too large- doorChance :: Rnd Bool,- doorOpenChance :: Rnd Bool,- doorSecretChance :: Rnd Bool,- doorSecretMax :: Int,- nrItems :: Rnd Int, -- range- depth :: Int -- general indicator of difficulty- }- -defaultLevelConfig :: Int -> LevelConfig-defaultLevelConfig d =- LevelConfig {- levelGrid = do- y <- randomR (2,4)- x <- randomR (3,5)- return (y,x),- minRoomSize = return (2,2),- darkRoomChance = chance $ 1%((22 - (2 * fromIntegral d)) `max` 2),- border = 2,- levelSize = (22,79),- extraConnects = \ (y,x) -> (y*x) `div` 3, - noRooms = \ (y,x) -> randomR (0,(y*x) `div` 3),- minStairsDistance = 676,- doorChance = chance $ 1%2,- doorOpenChance = chance $ 1%2,- doorSecretChance = chance $ 1%3,- doorSecretMax = 15,- nrItems = randomR (3,7),- depth = d- }--largeLevelConfig :: Int -> LevelConfig-largeLevelConfig d =- (defaultLevelConfig d) {- levelGrid = return (7,10),- levelSize = (77,231),- extraConnects = const 10- }--level :: LevelConfig ->- LevelName -> Rnd (Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) -> Level, Loc, Loc)-level cfg nm =- do- lgrid <- levelGrid cfg- lminroom <- minRoomSize cfg- let gs = M.toList (grid lgrid ((0,0),levelSize cfg))- -- grid locations of "no-rooms"- nrnr <- noRooms cfg lgrid- nr <- replicateM nrnr (do- let (y,x) = lgrid- yg <- randomR (0,y-1)- xg <- randomR (0,x-1)- return (yg,xg))- rs0 <- mapM (\ (i,r) -> do- r' <- if i `elem` nr- then mkNoRoom (border cfg) r- else mkRoom (border cfg) lminroom r- return (i,r')) gs- let rooms :: [(Loc, Loc)]- rooms = L.map snd rs0- dlrooms <- (mapM (\ r -> darkRoomChance cfg >>= \ c -> return (r, toDL (not c))) rooms) :: Rnd [((Loc, Loc), DL)]- let rs = M.fromList rs0- connects <- connectGrid lgrid- addedConnects <- replicateM (extraConnects cfg lgrid) (randomConnection lgrid)- let allConnects = L.nub (addedConnects ++ connects)- cs <- mapM- (\ (p0,p1) -> do- let r0 = rs ! p0- r1 = rs ! p1- connectRooms r0 r1) allConnects- let smap = M.fromList [ ((y,x),-100) | let (sy,sx) = levelSize cfg,- y <- [0..sy], x <- [0..sx] ]- let lmap :: LMap- lmap = foldr digCorridor (foldr (\ (r, dl) m -> digRoom dl r m) - (emptyLMap (levelSize cfg)) dlrooms) cs- let lvl = Level nm (levelSize cfg) [] smap lmap "" - -- convert openings into doors- dlmap <- fmap M.fromList . mapM- (\ o@((y,x),(t,r)) -> - case t of- Tile (Opening hv) _ ->- do- -- chance for doors- rb <- doorChance cfg- -- chance for a door to be open- ro <- doorOpenChance cfg- rs <- if ro then return Nothing- else do -- chance for a door to be secret- rsc <- doorSecretChance cfg- fmap Just- (if rsc then randomR (1, doorSecretMax cfg)- else return 0)- if rb- then return ((y,x),newTile (Door hv rs))- else return o- _ -> return o) .- M.toList $ lmap- -- determine number of items, items and locations for the items- nri <- nrItems cfg- is <- replicateM nri $- do- l <- findLoc lvl (const floor)- t <- newItem (depth cfg) itemFrequency - return (l,t)- -- locations of the stairs- su <- findLoc lvl (const floor)- sd <- findLoc lvl (\ l t -> floor t && distance (su,l) > minStairsDistance cfg)- let meta = show allConnects- return (\ lu ld ->- let flmap = maybe id (\ l -> M.update (\ (t,r) -> Just $ newTile (Stairs (toDL $ light t) Up l)) su) lu $- maybe id (\ l -> M.update (\ (t,r) -> Just $ newTile (Stairs (toDL $ light t) Down l)) sd) ld $- foldr (\ (l,it) f -> M.update (\ (t,r) -> Just (t { titems = it : titems t }, r)) l . f) id is- dlmap- in Level nm (levelSize cfg) [] smap flmap meta, su, sd)--emptyLMap :: (Y,X) -> LMap-emptyLMap (my,mx) = M.fromList [ ((y,x),newTile Rock) | x <- [0..mx], y <- [0..my] ]---- | If the room has size 1, it is assumed to be a no-room, and a single--- corridor field will be digged instead of a room.-digRoom :: DL -> Room -> LMap -> LMap-digRoom dl ((y0,x0),(y1,x1)) l- | y0 == y1 && x0 == x1 =- M.insert (y0,x0) (newTile Corridor) l- | otherwise =- let rm = M.fromList $ [ ((y,x),newTile (Floor dl)) | x <- [x0..x1], y <- [y0..y1] ]- ++ [ ((y,x),newTile (Wall p)) | (x,y,p) <- [(x0-1,y0-1,UL),(x1+1,y0-1,UR),(x0-1,y1+1,DL),(x1+1,y1+1,DR)] ]- ++ [ ((y,x),newTile (Wall p)) | x <- [x0..x1], (y,p) <- [(y0-1,U),(y1+1,D)] ]- ++ [ ((y,x),newTile (Wall p)) | (x,p) <- [(x0-1,L),(x1+1,R)], y <- [y0..y1] ]- in M.unionWith const rm l--addMonster :: Level -> Player -> Rnd Level-addMonster lvl@(Level { lmonsters = ms, lmap = lmap })- player@(Monster { mloc = ploc }) =- do- rc <- monsterGenChance (lname lvl) ms- if rc- then do- -- TODO: new monsters should always be generated in a place that isn't- -- visible by the player (if possible -- not possible for bigrooms)- sm <- findLoc lvl (\ l t -> floor t && - not (l `L.elem` L.map mloc (player : ms)) &&- distance (ploc, l) > 400)- m <- newMonster sm monsterFrequency- return (updateMonsters lvl (const (m : ms)))- else return lvl-
− FOV.hs
@@ -1,67 +0,0 @@-module FOV where--import Data.Map as M-import Data.Set as S-import Data.List as L-import Data.Ratio-import Debug.Trace--import Geometry-import Level--type Interval = (Rational, Rational)-type Distance = Int-type Progress = Int---- The current state of a scan is kept in a variable of Maybe Rational.--- If Just something, we're in a visible interval. If Nothing, we're in--- a shadowed interval.-scan :: ((Distance,Progress) -> Loc) -> LMap -> Distance -> Interval -> Set Loc-scan tr l d (s,e) = - let ps = downBias (s * fromIntegral d) -- minimal progress to check- pe = upBias (e * fromIntegral d) -- maximal progress to check- st = if open (l `at` tr (d,ps)) then Just s -- start in light- else Nothing -- start in shadow- in - -- trace (show (d,s,e,ps,pe)) $- S.union (S.fromList [tr (d,p) | p <- [ps..pe]]) (scan' st ps pe)- where- scan' :: Maybe Rational -> Progress -> Progress -> Set Loc- -- scan' st ps pe- -- | trace (show (st,ps,pe)) False = undefined- scan' (Just s) ps pe- | s >= e = S.empty -- empty interval- | ps > pe = scan tr l (d+1) (s,e) -- reached end, scan next- | closed (l `at` tr (d,ps)) =- let ne = (fromIntegral ps - (1%2)) / (fromIntegral d + (1%2))- in scan tr l (d+1) (s,ne) `S.union` scan' Nothing (ps+1) pe- -- entering shadow- | otherwise = scan' (Just s) (ps+1) pe- -- continue in light- scan' Nothing ps pe- | ps > pe = S.empty -- reached end while in shadow- | open (l `at` tr (d,ps)) = - let ns = (fromIntegral ps - (1%2)) / (fromIntegral d - (1%2))- in scan' (Just ns) (ps+1) pe- -- moving out of shadow- | otherwise = scan' Nothing (ps+1) pe- -- continue in shadow--tr0 (oy,ox) (d,p) = (oy + d,ox + p)-tr1 (oy,ox) (d,p) = (oy + d,ox - p)-tr2 (oy,ox) (d,p) = (oy - d,ox + p)-tr3 (oy,ox) (d,p) = (oy - d,ox - p)-tr4 (oy,ox) (d,p) = (oy + p,ox + d)-tr5 (oy,ox) (d,p) = (oy + p,ox - d)-tr6 (oy,ox) (d,p) = (oy - p,ox + d)-tr7 (oy,ox) (d,p) = (oy - p,ox - d)--fullscan loc lvl = - S.unions $- L.map (\ tr -> scan (tr loc) lvl 0 (0,1)) [tr0,tr1,tr2,tr3,tr4,tr5,tr6,tr7]---downBias, upBias :: (Integral a, Integral b) => Ratio a -> b-downBias x = round (x - 1 % (denominator x * 3))-upBias x = round (x + 1 % (denominator x * 3))-
− File.hs
@@ -1,22 +0,0 @@-module File where--import System.IO-import Data.Binary-import qualified Data.ByteString.Lazy as LBS-import Codec.Compression.Zlib as Z--strictReadCompressedFile :: FilePath -> IO LBS.ByteString-strictReadCompressedFile f =- do- h <- openBinaryFile f ReadMode- c <- LBS.hGetContents h- let d = Z.decompress c- LBS.length d `seq` return d--strictDecodeCompressedFile :: Binary a => FilePath -> IO a-strictDecodeCompressedFile = fmap decode . strictReadCompressedFile--encodeCompressedFile :: Binary a => FilePath -> a -> IO ()-encodeCompressedFile f = LBS.writeFile f . Z.compress . encode- -- note that LBS.writeFile opens the file in binary mode-
− Frequency.hs
@@ -1,31 +0,0 @@-module Frequency where--import Control.Monad--newtype Frequency a = Frequency { runFrequency :: [(Int, a)] }- deriving Show--instance Monad Frequency where- return x = Frequency [(1, x)]- m >>= f = Frequency- [(p * q, y) | (p, x) <- runFrequency m, - (q, y) <- runFrequency (f x) ]- fail "" = Frequency []--instance MonadPlus Frequency where- mplus (Frequency xs) (Frequency ys) = Frequency (xs ++ ys)- mzero = Frequency []--instance Functor Frequency where- fmap f (Frequency xs) = Frequency (map (\ (p, x) -> (p, f x)) xs)---- only try the second possibility if the first fails-melse :: Frequency a -> Frequency a -> Frequency a-melse (Frequency []) y = y-melse x y = x--scale :: Int -> Frequency a -> Frequency a-scale n (Frequency xs) = Frequency (map (\ (p, x) -> (n * p, x)) xs)--uniform :: [a] -> Frequency a-uniform = Frequency . map (\ x -> (1, x))
− Geometry.hs
@@ -1,60 +0,0 @@-module Geometry where---- | Game time in turns. (Placement in module Geometry is not ideal.)-type Time = Int--type X = Int-type Y = Int--type Loc = (Y,X)-type Dir = (Y,X)-type Area = ((Y,X),(Y,X))--towards :: (Loc,Loc) -> Dir-towards ((y0,x0),(y1,x1)) =- let dy = y1 - y0- dx = x1 - x0- angle = atan (fromIntegral dy / fromIntegral dx) / (pi / 2)- dir | angle <= -0.75 = (-1,0)- | angle <= -0.25 = (-1,1)- | angle <= 0.25 = (0,1)- | angle <= 0.75 = (1,1)- | angle <= 1.25 = (1,0)- | otherwise = (0,0)- in if dx >= 0 then dir else neg dir--distance :: (Loc,Loc) -> Int-distance ((y0,x0),(y1,x1)) = (y1 - y0)^2 + (x1 - x0)^2--adjacent :: Loc -> Loc -> Bool-adjacent s t = distance (s,t) <= 2--surroundings :: Loc -> [Loc]-surroundings l = map (l `shift`) moves--diagonal :: Loc -> Bool-diagonal (y,x) = y*x /= 0--shift :: Loc -> Dir -> Loc-shift (y0,x0) (y1,x1) = (y0+y1,x0+x1)--neg :: Dir -> Dir-neg (y,x) = (-y,-x)--moves :: [Dir]-moves = [ (x,y) | x <- [-1..1], y <- [-1..1], x /= 0 || y /= 0 ]--up, down, left, right :: Dir-upleft, upright, downleft, downright :: Dir-upleft = up `shift` left-upright = up `shift` right-downleft = down `shift` left-downright = down `shift` right-up = (-1,0)-down = (1,0)-left = (0,-1)-right = (0,1)--horiz, vert :: [Dir]-horiz = [left, right]-vert = [up, down]
− Item.hs
@@ -1,234 +0,0 @@-module Item where--import Data.Binary-import Data.Map as M-import Data.Set as S-import Data.List as L-import Data.Maybe-import Data.Char-import Control.Monad--import Display-import Geometry-import Random--data Item = Item- { icount :: Int,- itype :: ItemType, - iletter :: Maybe Char } -- inventory identifier- deriving Show--data ItemType =- Ring- | Scroll- | Potion PotionType- | Wand- | Amulet- | Gem- | Gold- deriving (Eq, Ord, Show)--data PotionType =- PotionWater- | PotionHealing- deriving (Eq, Ord, Show)--data Appearance =- Clear- | White- deriving (Eq, Show)--type Assocs = M.Map ItemType Appearance-type Discoveries = S.Set ItemType--potionType :: PotionType -> String -> String-potionType PotionWater s = s ++ " of water"-potionType PotionHealing s = s ++ " of healing"--appearance :: Appearance -> String -> String-appearance Clear s = "clear " ++ s-appearance White s = "white " ++ s--instance Binary Item where- put (Item icount itype iletter) = put icount >> put itype >> put iletter- get = liftM3 Item get get get--instance Binary ItemType where- put Ring = putWord8 0- put Scroll = putWord8 1- put (Potion t) = putWord8 2 >> put t- put Wand = putWord8 3- put Amulet = putWord8 4- put Gem = putWord8 5- put Gold = putWord8 6- get = do- tag <- getWord8- case tag of- 0 -> return Ring- 1 -> return Scroll- 2 -> liftM Potion get- 3 -> return Wand- 4 -> return Amulet- 5 -> return Gem- 6 -> return Gold--instance Binary PotionType where- put PotionWater = putWord8 0- put PotionHealing = putWord8 1- get = do- tag <- getWord8- case tag of- 0 -> return PotionWater- 1 -> return PotionHealing--instance Binary Appearance where- put Clear = putWord8 0- put White = putWord8 1- get = do- tag <- getWord8- case tag of- 0 -> return Clear- 1 -> return White--itemFrequency :: Frequency ItemType-itemFrequency =- Frequency- [- (100, Gold),- (30, Gem),- (20, Ring),- (40, Scroll),- (20, Wand),- (10, Amulet),- (30, Potion PotionWater),- (10, Potion PotionHealing)- ]--itemQuantity :: Int -> ItemType -> Rnd Int-itemQuantity n Gold = (2 * n) *~ d 8-itemQuantity _ _ = return 1--itemLetter :: ItemType -> Maybe Char-itemLetter Gold = Just '$'-itemLetter _ = Nothing---- | Generate an item.-newItem :: Int -> Frequency ItemType -> Rnd Item-newItem n ftp =- do- tp <- frequency ftp- nr <- itemQuantity n tp- return (Item nr tp (itemLetter tp))---- | Assigns a letter to an item, for inclusion--- in the inventory of the player. Takes a remembered--- letter and a starting letter.-assignLetter :: Maybe Char -> Char -> [Item] -> Maybe Char-assignLetter r c is =- case r of- Just l | l `L.elem` allowed -> Just l- _ -> listToMaybe free- - where- current = S.fromList (concatMap (maybeToList . iletter) is)- allLetters = ['a'..'z'] ++ ['A'..'Z']- candidates = take (length allLetters) (drop (fromJust (L.findIndex (==c) allLetters)) (cycle allLetters))- free = L.filter (\x -> not (x `S.member` current)) candidates- allowed = '$' : free--cmpLetter :: Char -> Char -> Ordering-cmpLetter x y = compare (isUpper x, toLower x) (isUpper y, toLower y)--cmpLetter' :: Maybe Char -> Maybe Char -> Ordering-cmpLetter' Nothing Nothing = EQ-cmpLetter' Nothing (Just _) = GT-cmpLetter' (Just _) Nothing = LT-cmpLetter' (Just l) (Just l') = cmpLetter l l'--maxBy :: (a -> a -> Ordering) -> a -> a -> a-maxBy cmp x y = case cmp x y of- LT -> y- _ -> x--maxLetter = maxBy cmpLetter--mergeLetter :: Maybe Char -> Maybe Char -> Maybe Char-mergeLetter = mplus--letterRange :: [Char] -> String-letterRange xs = sectionBy (sortBy cmpLetter xs) Nothing- where- succLetter c d = ord d - ord c == 1-- sectionBy [] Nothing = ""- sectionBy [] (Just (c,d)) = finish (c,d)- sectionBy (x:xs) Nothing = sectionBy xs (Just (x,x))- sectionBy (x:xs) (Just (c,d)) | succLetter d x- = sectionBy xs (Just (c,x))- | otherwise- = finish (c,d) ++ sectionBy xs (Just (x,x))-- finish (c,d) | c == d = [c]- | succLetter c d = [c,d]- | otherwise = [c,'-',d]--letterLabel :: Maybe Char -> String-letterLabel Nothing = " "-letterLabel (Just c) = c : " - "--viewItem :: ItemType -> Assocs -> (Char, Attr -> Attr)-viewItem i a = viewItem' i (M.lookup i a)- where- viewItem' Ring _ = ('=', id)- viewItem' Scroll _ = ('?', id)- viewItem' (Potion {}) (Just Clear) = ('!', setBold . setFG blue)- viewItem' (Potion {}) (Just White) = ('!', setBold . setFG white)- viewItem' (Potion {}) _ = ('!', id)- viewItem' Wand _ = ('/', id)- viewItem' Gold _ = ('$', setBold . setFG yellow)- viewItem' Gem _ = ('*', setFG red)- viewItem' Amulet _ = ('"', id)- viewItem' _ _ = ('~', id)---- | Adds an item to a list of items, joining equal items.--- Also returns the joined item.-joinItem :: Item -> [Item] -> (Item,[Item])-joinItem i is = case findItem (\ j -> itype i == itype j) is of- Nothing -> (i, i : is)- Just (j,js) -> let n = i { icount = icount i + icount j,- iletter = mergeLetter (iletter j) (iletter i) }- in (n, n : js)---- | Finds an item in a list of items.-findItem :: (Item -> Bool) -> [Item] -> Maybe (Item, [Item])-findItem p is = findItem' [] is- where- findItem' acc [] = Nothing- findItem' acc (i:is)- | p i = Just (i, reverse acc ++ is)- | otherwise = findItem' (i:acc) is--makeObject :: Int -> (String -> String) -> String -> String-makeObject 1 adj obj = let b = adj obj- in case b of- (c:_) | c `elem` "aeio" -> "an " ++ b- _ -> "a " ++ b-makeObject n adj obj = show n ++ " " ++ adj (obj ++ "s")--objectItem :: Assocs -> Discoveries -> Int -> ItemType -> String-objectItem _ _ n Ring = makeObject n id "ring"-objectItem _ _ n Scroll = makeObject n id "scroll"-objectItem a d n (Potion t) = makeObject n (identified a d (Potion t)) "potion"-objectItem _ _ n Wand = makeObject n id "wand"-objectItem _ _ n Amulet = makeObject n id "amulet"-objectItem _ _ n Gem = makeObject n id "gem"-objectItem _ _ n Gold = makeObject n id "gold piece"--identified :: Assocs -> Discoveries -> ItemType -> String -> String-identified a d i- | i `S.member` d = case i of- Potion t -> potionType t- _ -> ("really strange " ++)- | otherwise = case M.lookup i a of- Just ap -> appearance ap- _ -> ("really strange " ++)
+ LICENSE view
@@ -0,0 +1,28 @@+Copyright (c) 2008--2011 Andres Loeh+Copyright (c) 2010--2011 Mikolaj Konarski++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.+3. Neither the name of the author nor the names of his contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF+SUCH DAMAGE.
LambdaHack.cabal view
@@ -1,9 +1,10 @@ cabal-version: >= 1.2 name: LambdaHack-version: 0.1.20090606-license: GPL-license-file: COPYING-data-files: README+version: 0.1.20110117+license: BSD3+license-file: LICENSE+data-files: LICENSE, CREDITS, DESIGN.markdown, PLAYING.markdown,+ README.markdown, LambdaHack.config.example author: Andres Loeh <mail@andres-loeh.de> maintainer: Andres Loeh <mail@andres-loeh.de> description: a small roguelike game@@ -15,31 +16,37 @@ description: enable curses support default: False -flag gtk- description: enable gtk support- default: True+flag vty+ description: enable vty support+ default: False executable LambdaHack main-is: LambdaHack.hs- other-modules: Actor, Display, Display2, Dungeon, File,- FOV, Frequency, Geometry, Item, LambdaHack,- Level, Message, Monster, Perception, Random,- Save, Setup, State, Strategy, Style, Turn,- Version- build-depends: base >= 3 && < 4, containers >= 0.1 && < 1, binary >= 0.4 && < 1,- random >= 1 && < 2, zlib >= 0.4 && < 1, bytestring >= 0.9 && < 1,- directory >= 1 && < 2, mtl >= 1.1 && < 2- extensions: CPP+ hs-source-dirs:src+ other-modules: Actor, Action, Actions, Command, Display, Display2, Dungeon,+ File, FOV, Frequency, Geometry, Item, ItemState,+ Keys, Keybindings, LambdaHack, Level, LevelState, Message,+ Monster, Perception, Random, Save, State, Strategy,+ StrategyState, Turn, Version, HighScores, Config,+ Grammar+ build-depends: base >= 4 && <5, containers >= 0.1 && < 1,+ binary >= 0.4 && < 1,+ random >= 1 && < 2, zlib >= 0.4 && < 1,+ bytestring >= 0.9 && < 1, directory >= 1 && < 2,+ mtl >= 1.1 && < 3, old-time, ConfigFile >= 1.0.6 && < 2,+ MissingH >= 1.1.0.3 && < 1.2, filepath >= 1.1.0.3 && < 1.2+ extensions: CPP, FlexibleContexts if flag(curses) { other-modules: Display.Curses build-depends: hscurses >= 1.3 && < 2 cpp-options: -DCURSES extra-libraries: curses- } else { if flag(gtk) {+ } else { if flag(vty) {+ other-modules: Display.Vty+ build-depends: vty >= 4.4+ } else { other-modules: Display.Gtk- build-depends: gtk >= 0.9.12 && < 0.11+ build-depends: gtk >= 0.11 && < 0.13 cpp-options: -DGTK- } else {- other-modules: Display.Vty- build-depends: vty >= 3+ ghc-options: -threaded } }
+ LambdaHack.config.example view
@@ -0,0 +1,25 @@+# LambdaHack looks for this file in ~/.LambdaHack/LambdaHack.config++# If you contribute to LambdaHack, please create directory ~/.LambdaHack/+# and move this example config file to ~/.LambdaHack/LambdaHack.config.+# Optionally, also copy LambdaHack.scores to ~/.LambdaConfig/.+# In this way, you won't accidentally commit your private high scores+# (nor your save files) to LambdaHack git repository.++# paths to various game files; relative to ~/.LambdaHack/+# (or analogous prefixes for other OSes, see getAppUserDataDirectory)+[files]+savegame: LambdaHack.save+highscores: LambdaHack.scores++[engine]+fov_mode: shadow+#fov_mode: diagonal+#fov_mode: permissive+fov_radius: 40++[dungeon]+depth: 10+level3: bigroom+level10: noiseroom+#level1: noiseroom
− LambdaHack.hs
@@ -1,61 +0,0 @@-module Main where--import System.Directory-import Control.Monad-import Data.Map as M--import State-import Geometry-import Level-import Dungeon-import Perception-import Display2-import Random-import Save-import Turn-import Item--main :: IO ()-main = startup start---- | Either restore a saved game, or setup a new game.-start :: Session -> IO ()-start session =- do- -- check if we have a savegame- x <- doesFileExist savefile- restored <- if x then do- displayBlankConfirm session "Restoring save game"- restoreGame- else return $ Right "Welcome to LambdaHack!" -- new game- case restored of- Right msg -> generate session msg- Left (lvl,state) -> handle session lvl state (perception_ state lvl)- "Welcome back to LambdaHack."---- | Generate the dungeon for a new game, and start the game loop.-generate :: Session -> String -> IO ()-generate session msg =- do- -- generate dungeon with 10 levels- levels <- rndToIO $- mapM (\n -> (if n == 3 then bigroom else level) (defaultLevelConfig n) $- LambdaCave n) [1..10]- let connect :: Maybe (Maybe DungeonLoc) ->- [(Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) -> Level, Loc, Loc)] ->- [Level]- connect au [(x,_,_)] = [x au Nothing]- connect au ((x,_,d):ys@((_,u,_):_)) =- let (z:zs) = connect (Just (Just (lname x',d))) ys- x' = x au (Just (Just (lname z,u)))- in x' : z : zs- let lvls = connect (Just Nothing) levels- let (lvl,dng) = (head lvls, dungeon (tail lvls))- -- generate item associations- let assocs = M.fromList- [ (Potion PotionWater, Clear),- (Potion PotionHealing, White) ]- let state = (defaultState ((\ (_,x,_) -> x) (head levels)) dng)- { sassocs = assocs }- handle session lvl state (perception_ state lvl) msg-
− Level.hs
@@ -1,512 +0,0 @@-module Level where--import qualified System.Random as R-import Control.Monad--import Data.Binary-import Data.Map as M-import Data.Set as S-import Data.List as L-import Data.Ratio-import Data.Maybe--import Geometry-import Monster-import Item-import Random-import Display---- | Names of the dungeon levels are represented using a--- custom data structure.-data LevelName = LambdaCave Int | Exit- deriving (Show, Eq, Ord)---- | Chance that a new monster is generated. Currently depends on the--- number of monsters already present, and on the level. In the future,--- the strength of the character and the strength of the monsters present--- could further influence the chance, and the chance could also affect--- which monster is generated.-monsterGenChance :: LevelName -> [Monster] -> Rnd Bool-monsterGenChance (LambdaCave n) [] = chance $ 1%50-monsterGenChance (LambdaCave n) _ = chance $ 1%((1000 - (fromIntegral n * 50)) `max` 300)-monsterGenChance _ _ = return False--instance Binary LevelName where- put (LambdaCave n) = put n- get = liftM LambdaCave get---- | Provide a textual description of a level name.-levelName :: LevelName -> String-levelName (LambdaCave n) = "The Lambda Cave " ++ show n---- | The complete dungeon is a map from level names to levels.--- We usually store all but the current level in this data structure.-data Dungeon = Dungeon (M.Map LevelName Level)- deriving Show---- | Create a dungeon from a list of levels.-dungeon :: [Level] -> Dungeon-dungeon = Dungeon . M.fromList . L.map (\ l -> (lname l, l))---- | Extract a level from a dungeon.-getDungeonLevel :: LevelName -> Dungeon -> (Level, Dungeon)-getDungeonLevel ln (Dungeon dng) = (fromJust (M.lookup ln dng), Dungeon (M.delete ln dng))---- | Put a level into a dungeon.-putDungeonLevel :: Level -> Dungeon -> Dungeon-putDungeonLevel lvl (Dungeon dng) = Dungeon (M.insert (lname lvl) lvl dng)--instance Binary Dungeon where- put (Dungeon dng) = put (M.elems dng)- get = liftM dungeon get---- | A dungeon location is a level together with a location on--- that level.-type DungeonLoc = (LevelName, Loc)--data Level = Level- { lname :: LevelName,- lsize :: (Y,X),- lmonsters :: [Monster],- lsmell :: SMap,- lmap :: LMap,- lmeta :: String }- deriving Show--updateLMap :: Level -> (LMap -> LMap) -> Level-updateLMap lvl f = lvl { lmap = f (lmap lvl) }--updateMonsters :: Level -> ([Monster] -> [Monster]) -> Level-updateMonsters lvl f = lvl { lmonsters = f (lmonsters lvl) }--instance Binary Level where- put (Level nm sz@(sy,sx) ms lsmell lmap lmeta) = - do- put nm- put sz- put ms- put [ lsmell ! (y,x) | y <- [0..sy], x <- [0..sx] ]- put [ lmap ! (y,x) | y <- [0..sy], x <- [0..sx] ]- put lmeta- get = do- nm <- get- sz@(sy,sx) <- get- ms <- get- xs <- get- let lsmell = M.fromList (zip [ (y,x) | y <- [0..sy], x <- [0..sx] ] xs)- xs <- get- let lmap = M.fromList (zip [ (y,x) | y <- [0..sy], x <- [0..sx] ] xs)- lmeta <- get- return (Level nm sz ms lsmell lmap lmeta)--type LMap = Map (Y,X) (Tile,Tile)-type SMap = Map (Y,X) Time--data Tile = Tile- { tterrain :: Terrain,- titems :: [Item] }- deriving Show--instance Binary Tile where- put (Tile t is) = put t >> put is- get = liftM2 Tile get get--at l p = fst (findWithDefault (unknown, unknown) p l)-rememberAt l p = snd (findWithDefault (unknown, unknown) p l)--unknown :: Tile-unknown = Tile Unknown []--data Terrain = Rock- | Opening Pos- | Floor DL- | Unknown- | Corridor- | Wall Pos- | Stairs DL VDir (Maybe DungeonLoc)- | Door Pos (Maybe Int) -- Nothing: open, Just 0: closed, otherwise secret- deriving Show--instance Binary Terrain where- put Rock = putWord8 0- put (Opening p) = putWord8 1 >> put p- put (Floor dl) = putWord8 2 >> put dl- put Unknown = putWord8 3- put Corridor = putWord8 4- put (Wall p) = putWord8 5 >> put p- put (Stairs dl d n) = putWord8 6 >> put dl >> put d >> put n- put (Door p o) = putWord8 7 >> put p >> put o- get = do- tag <- getWord8- case tag of- 0 -> return Rock- 1 -> liftM Opening get- 2 -> liftM Floor get- 3 -> return Unknown- 4 -> return Corridor- 5 -> liftM Wall get- 6 -> liftM3 Stairs get get get- 7 -> liftM2 Door get get- _ -> fail "no parse (Tile)"--data DL = Dark | Light- deriving (Eq, Show, Bounded)---- | All the wall types that are possible:------ * 'UL': upper left------ * 'U': upper------ * 'UR': upper right------ * 'L': left------ * 'R': right------ * 'DL': lower left------ * 'D': lower------ * 'DR': lower right------ I am tempted to add even more (T-pieces and crossings),--- but currently, we don't need them.-data Pos = UL | U | UR | L | R | DL | D | DR- deriving (Eq, Show, Bounded)--instance Binary Pos where- put UL = putWord8 0- put U = putWord8 1- put UR = putWord8 2- put L = putWord8 3- put R = putWord8 4- put DL = putWord8 5- put D = putWord8 6- put DR = putWord8 7-- get = do- tag <- getWord8- case tag of- 0 -> return UL- 1 -> return U- 2 -> return UR- 3 -> return L- 4 -> return R- 5 -> return DL- 6 -> return D- 7 -> return DR--data HV = Horiz | Vert- deriving (Eq, Show, Bounded)--fromHV Horiz = True-fromHV Vert = False--toHV True = Horiz-toHV False = Vert--instance R.Random HV where- randomR (a,b) g = case R.randomR (fromHV a,fromHV b) g of- (b,g') -> (toHV b,g')- random g = R.randomR (minBound, maxBound) g--instance Binary HV where- put Horiz = put True- put Vert = put False- get = get >>= \ b -> if b then return Horiz else return Vert--instance Binary DL where- put Dark = put False- put Light = put True- get = get >>= \ b -> if b then return Light else return Dark--data VDir = Up | Down- deriving (Eq, Show)--instance Binary VDir where- put Up = put True- put Down = put False- get = get >>= \ b -> if b then return Up else return Down--instance Eq Terrain where- Rock == Rock = True- Opening d == Opening d' = d == d'- Floor l == Floor l' = l == l'- Unknown == Unknown = True- Corridor == Corridor = True- Wall p == Wall p' = p == p'- Stairs dl d t == Stairs dl' d' t' = dl == dl' && d == d' && t == t'- Door p o == Door p' o' = p == p' && o == o'- _ == _ = False---- | blocks moves and vision-closed :: Tile -> Bool-closed = not . open--floor :: Tile -> Bool-floor (Tile { tterrain = Floor _ }) = True-floor _ = False--secret :: Maybe Int -> Bool-secret (Just n) | n /= 0 = True-secret _ = False--toOpen :: Bool -> Maybe Int-toOpen True = Nothing-toOpen False = Just 0--fromDL :: DL -> Bool-fromDL Dark = False-fromDL Light = True--toDL :: Bool -> DL-toDL False = Dark-toDL True = Light---- | allows moves and vision-open :: Tile -> Bool-open (Tile (Floor {}) _) = True-open (Tile (Opening {}) _) = True-open (Tile (Door _ o) _) = isNothing o-open (Tile Corridor _) = True-open (Tile (Stairs {}) _) = True-open _ = False---- | is lighted on its own-light :: Tile -> Bool-light (Tile (Floor l) _) = fromDL l-light (Tile (Stairs l _ _) _) = fromDL l-light _ = False---- | Passive tiles reflect light from some other (usually adjacent)--- positions. This function returns the offsets from which light is--- reflected. Not all passively lighted tiles reflect from all directions.--- Walls, for instance, cannot usually be seen from the outside.-passive :: Tile -> [Dir] -passive (Tile (Wall p) _) = posToDir p-passive (Tile (Opening _) _) = moves-passive (Tile (Door p Nothing) _) = moves-passive (Tile (Door p (Just 0)) _) = moves- -- doors can be seen from all sides-passive (Tile (Door p (Just n)) _) = posToDir p- -- secret doors are like walls-passive (Tile (Stairs _ _ _) _) = moves-passive _ = []---- | Perceptible is similar to passive, but describes which tiles can--- be seen from which adjacent fields in the dark.-perceptible :: Tile -> [Dir]-perceptible (Tile Rock _) = []-perceptible p = case passive p of- [] -> moves- ds -> ds---- | Maps wall types to lists of expected floor positions.-posToDir :: Pos -> [Dir]-posToDir UL = [downright]-posToDir U = [down]-posToDir UR = [downleft]-posToDir L = [right]-posToDir R = [left]-posToDir DL = [upright]-posToDir D = [up]-posToDir DR = [upleft]---- checks for the presence of monsters (and items); it does *not* check--- if the tile is open ...-unoccupied :: [Monster] -> LMap -> Loc -> Bool-unoccupied monsters lvl loc =- all (\ m -> mloc m /= loc) monsters---- check whether one location is accessible from the other--- precondition: the two locations are next to each other--- currently only implements that doors aren't accessible diagonally,--- and that the target location has to be open-accessible :: LMap -> Loc -> Loc -> Bool-accessible lvl source target =- let dir = shift source (neg target)- src = lvl `at` source- tgt = lvl `at` target- in open tgt &&- (not (diagonal dir) || - case (tterrain src, tterrain tgt) of- (Door {}, _) -> False- (_, Door {}) -> False- _ -> True)--findLocInArea :: Area -> (Loc -> Bool) -> Rnd Loc-findLocInArea a@((y0,x0),(y1,x1)) p =- do- rx <- randomR (x0,x1)- ry <- randomR (y0,y1)- let loc = (ry,rx)- if p loc then return loc else findLocInArea a p--locInArea :: Area -> Rnd Loc-locInArea a = findLocInArea a (const True)--findLoc :: Level -> (Loc -> Tile -> Bool) -> Rnd Loc-findLoc l@(Level { lsize = sz, lmap = lm }) p =- do- loc <- locInArea ((0,0),sz)- if p loc (lm `at` loc) then return loc- else findLoc l p--grid :: (Y,X) -> Area -> Map (Y,X) Area-grid (ny,nx) ((y0,x0),(y1,x1)) =- let yd = y1 - y0- xd = x1 - x0- in M.fromList [ ((y,x), ((y0 + (yd * y `div` ny), x0 + (xd * x `div` nx)),- (y0 + (yd * (y + 1) `div` ny - 1), x0 + (xd * (x + 1) `div` nx - 1))))- | x <- [0..nx-1], y <- [0..ny-1] ]---connectGrid :: (Y,X) -> Rnd [((Y,X),(Y,X))]-connectGrid (ny,nx) =- do- let unconnected = S.fromList [ (y,x) | x <- [0..nx-1], y <- [0..ny-1] ]- -- candidates are neighbors that are still unconnected; we start with- -- a random choice- rx <- randomR (0,nx-1)- ry <- randomR (0,ny-1)- let candidates = S.fromList [ (ry,rx) ]- connectGrid' (ny,nx) unconnected candidates []--randomConnection :: (Y,X) -> Rnd ((Y,X),(Y,X))-randomConnection (ny,nx) =- do- rb <- randomR (False,True)- if rb then do- rx <- randomR (0,nx-2)- ry <- randomR (0,ny-1)- return (normalize ((ry,rx),(ry,rx+1)))- else do- ry <- randomR (0,ny-2)- rx <- randomR (0,nx-1)- return (normalize ((ry,rx),(ry+1,rx)))--normalize :: ((Y,X),(Y,X)) -> ((Y,X),(Y,X))-normalize (a,b) | a <= b = (a,b)- | otherwise = (b,a)--normalizeArea :: Area -> Area-normalizeArea a@((y0,x0),(y1,x1)) = ((min y0 y1, min x0 x1), (max y0 y1, max x0 x1))--connectGrid' :: (Y,X) -> Set (Y,X) -> Set (Y,X) -> [((Y,X),(Y,X))] -> Rnd [((Y,X),(Y,X))]-connectGrid' (ny,nx) unconnected candidates acc- | S.null candidates = return (L.map normalize acc)- | otherwise = do- c <- oneOf (S.toList candidates)- let ns = neighbors ((0,0),(ny-1,nx-1)) c -- potential new candidates- let nu = S.delete c unconnected -- new unconnected- let (nc,ds) = S.partition (`S.member` nu) ns- -- (new candidates, potential connections)- new <- if S.null ds then return id- else do- d <- oneOf (S.toList ds)- return ((c,d) :)- connectGrid' (ny,nx) nu- (S.delete c (candidates `S.union` nc)) (new acc)--neighbors :: Area -> {- size limitation -}- Loc -> {- location to find neighbors of -}- Set Loc-neighbors area (y,x) =- let cs = [ (y + dy, x + dx) | dy <- [-1..1], dx <- [-1..1], (dx + dy) `mod` 2 == 1 ] - in S.fromList (L.filter (`inside` area) cs)--inside :: Loc -> Area -> Bool-inside (y,x) ((y0,x0),(y1,x1)) = x1 >= x && x >= x0 && y1 >= y && y >= y0---fromTo :: Loc -> Loc -> [Loc]-fromTo (y0,x0) (y1,x1)- | y0 == y1 = L.map (\ x -> (y0,x)) (fromTo1 x0 x1)- | x0 == x1 = L.map (\ y -> (y,x0)) (fromTo1 y0 y1)--fromTo1 :: X -> X -> [X]-fromTo1 x0 x1- | x0 <= x1 = [x0..x1]- | otherwise = [x0,x0-1..x1]--viewTile :: Bool -> Tile -> Assocs -> (Char, Attr -> Attr)-viewTile b (Tile t []) a = viewTerrain 0 b t -viewTile b (Tile t (i:_)) a = viewItem (itype i) a---- | Produces a textual description of the items at a location. It's--- probably correct to use 'at' rather than 'rememberAt' at this point,--- although we could argue that 'rememberAt' reflects what the player can--- perceive more correctly ...------ The "detailed" variant is for use with an explicit look command.-lookAt :: Bool -> Assocs -> Discoveries -> LMap -> Loc -> String-lookAt detailed a d lvl loc- | detailed = lookTerrain (tterrain (lvl `at` loc)) ++ " " ++ isd- | otherwise = isd- where- is = titems (lvl `at` loc)- isd = case is of- [] -> ""- [i] -> "You see " ++ objectItem a d (icount i) (itype i) ++ "."- [i,j] -> "You see " ++ objectItem a d (icount i) (itype i) ++ " and "- ++ objectItem a d (icount j) (itype j) ++ "."- _ -> "There are several objects here" ++- if detailed then ":" else "."---- | Produces a textual description for terrain, used if no objects--- are present.-lookTerrain :: Terrain -> String-lookTerrain (Floor _) = "Floor."-lookTerrain Corridor = "Corridor."-lookTerrain (Opening _) = "An opening."-lookTerrain (Stairs _ Up _) = "A staircase up."-lookTerrain (Stairs _ Down _) = "A staircase down."-lookTerrain (Door _ Nothing) = "An open door."-lookTerrain _ = ""---- | The parameter "n" is the level of evolution:------ 0: final--- 1: stairs added--- 2: doors added--- 3: corridors and openings added--- 4: only rooms------ The Bool indicates whether the loc is currently visible.-viewTerrain :: Int -> Bool -> Terrain -> (Char, Attr -> Attr)-viewTerrain n b Rock = (' ', id)-viewTerrain n b (Opening d)- | n <= 3 = ('.', id)- | otherwise = viewTerrain 0 b (Wall d)-viewTerrain n b (Floor Light) = ('.', id)-viewTerrain n b (Floor Dark) = if b then ('.', id) else (' ', id)-viewTerrain n b Unknown = (' ', id)-viewTerrain n b Corridor- | n <= 3 = ('#', id)- | otherwise = viewTerrain 0 b Rock-viewTerrain n b (Wall p)- | p `elem` [L, R] = ('|', id)- | otherwise = ('-', id)-viewTerrain n b (Stairs _ Up _)- | n <= 1 = ('<', id)- | otherwise = viewTerrain 0 b (Floor Dark)-viewTerrain n b (Stairs _ Down _)- | n <= 1 = ('>', id)- | otherwise = viewTerrain 0 b (Floor Dark)-viewTerrain n b (Door d (Just 0))- | n <= 2 = ('+', setFG yellow)- | otherwise = viewTerrain n b (Opening d)-viewTerrain n b (Door d (Just _))- | n <= 2 = viewTerrain n b (Wall d) -- secret door- | otherwise = viewTerrain n b (Opening d)-viewTerrain n b (Door p Nothing)- | n <= 2 = (if p `elem` [L, R] then '-' else '|', setFG yellow)- | otherwise = viewTerrain n b (Opening p)--viewSmell :: Int -> (Char, Attr -> Attr)-viewSmell n = let k | n > 9 = '*'- | n < 0 = '-'- | otherwise = head . show $ n- in (k, setFG black . setBG green)-
− Message.hs
@@ -1,28 +0,0 @@-module Message where--import Data.List as L-import Data.Char--type Message = String--more :: Message-more = " --more--"--addMsg :: Message -> Message -> Message-addMsg [] x = x-addMsg xs [] = xs-addMsg xs x = xs ++ " " ++ x--splitMsg :: Int -> Message -> [String]-splitMsg w xs- | w <= m = [xs] -- border case, we cannot make progress- | l <= w = [xs] -- no problem, everything fits- | otherwise = let (pre, post) = splitAt (w - m) xs- (ppre, ppost) = break (`L.elem` " .,:!;") $ reverse pre- rpost = dropWhile isSpace ppost- in if L.null rpost then pre : splitMsg w post- else reverse rpost : splitMsg w (reverse ppre ++ post)- where- m = length more- l = length xs -
− Monster.hs
@@ -1,147 +0,0 @@-module Monster where--import Data.Char-import Data.Binary-import Control.Monad--import Geometry-import Display-import Item-import Random---- | Hit points of the player. TODO: Should not be hardcoded.-playerHP :: Int-playerHP = 20---- | Time the player can be traced by monsters. TODO: Make configurable.-smellTimeout :: Time-smellTimeout = 1000---- | Initial player.-defaultPlayer :: Loc -> Player-defaultPlayer ploc =- Monster Player playerHP Nothing ploc [] 'a' 10 0--type Player = Monster--data Monster = Monster- { mtype :: !MonsterType,- mhp :: !Int,- mdir :: Maybe Dir, -- for monsters: the dir the monster last moved;- -- for the player: the dir the player is running- mloc :: !Loc,- mitems :: [Item], -- inventory- mletter :: !Char, -- next inventory letter- mspeed :: !Time, -- speed (i.e., delay before next action)- mtime :: !Time } -- time of next action- deriving Show--instance Binary Monster where- put (Monster mt mhp md ml minv mletter mspeed mtime) =- do- put mt- put mhp- put md- put ml- put minv- put mletter- put mspeed- put mtime- get = do- mt <- get- mhp <- get- md <- get- ml <- get- minv <- get- mletter <- get- mspeed <- get- mtime <- get- return (Monster mt mhp md ml minv mletter mspeed mtime)--data MonsterType =- Player- | Eye- | FastEye- | Nose- deriving (Show, Eq)--instance Binary MonsterType where- put Player = putWord8 0 - put Eye = putWord8 1- put FastEye = putWord8 2- put Nose = putWord8 3- get = do- tag <- getWord8- case tag of- 0 -> return Player - 1 -> return Eye- 2 -> return FastEye- 3 -> return Nose- _ -> fail "no parse (MonsterType)" --monsterFrequency :: Frequency MonsterType-monsterFrequency =- Frequency- [ - (2, Nose),- (6, Eye),- (1, FastEye)- ]---- | Generate monster.-newMonster :: Loc -> Frequency MonsterType -> Rnd Monster-newMonster loc ftp =- do- tp <- frequency ftp- hp <- hps tp- let s = speed tp- return (template tp hp loc s)- where- -- setting the time of new monsters to 0 makes them able to- -- move immediately after generation; this does not seem like- -- a bad idea, but it would certainly be "more correct" to set- -- the time to the creation time instead- template tp hp loc s = Monster tp hp Nothing loc [] 'a' s 0- - hps Eye = randomR (1,3)- hps FastEye = randomR (1,3)- hps Nose = randomR (2,3)-- speed Eye = 10- speed FastEye = 3- speed Nose = 11---- | Insert a monster in an mtime-sorted list of monsters.--- Returns the position of the inserted monster and the new list.-insertMonster :: Monster -> [Monster] -> (Int, [Monster])-insertMonster = insertMonster' 0- where- insertMonster' n m [] = (n, [m])- insertMonster' n m (m':ms)- | mtime m <= mtime m' = (n, m : m' : ms)- | otherwise = let (n', ms') = insertMonster' (n + 1) m ms- in (n', m' : ms')---objectMonster :: MonsterType -> String-objectMonster Player = "you"-objectMonster Eye = "the reducible eye"-objectMonster FastEye = "the super-fast eye"-objectMonster Nose = "the point-free nose"--subjectMonster :: MonsterType -> String-subjectMonster x = let (s:r) = objectMonster x in toUpper s : r--verbMonster :: MonsterType -> String -> String-verbMonster Player v = v-verbMonster _ v = v ++ "s"--compoundVerbMonster :: MonsterType -> String -> String -> String-compoundVerbMonster Player v p = v ++ " " ++ p-compoundVerbMonster _ v p = v ++ "s " ++ p--viewMonster :: MonsterType -> (Char, Attr -> Attr)-viewMonster Player = ('@', setBG white . setFG black)-viewMonster Eye = ('e', setFG red)-viewMonster FastEye = ('e', setFG blue)-viewMonster Nose = ('n', setFG green)
+ PLAYING.markdown view
@@ -0,0 +1,111 @@+Playing LambdaHack+==================++Playing the game consist of walking around the dungeon and bumping+into things (doors, monsters, treasure). Once the few basic command keys+and on-screen symbols are learned, mastery and enjoyment of the game+is the matter of tactical skill and literary imagination.++To be honest, right now you need a lot of imagination, since the game+is very basic, though playable and winnable. Contributions welcome.+++Dungeon+-------++The goal of the hero is to explore the dungeon from top to the very bottom+(and grab lots of shiny treasure and gear on the way).+The dungeon consists of 10 levels and each level consists of 80 by 21 tiles.+The basic tiles tiles are as follows:++ dungeon terrain type on-screen symbol+ floor .+ wall (horizontal and vertical) - and |+ corridor #+ stairs (up and down) < and >+ closed door ++ rock blank++The game world is persistent, i.e., every time the hero visits a level+during one game, the level should look the same.+++Keys+----++Here are a few keys you can use in the game:++ key command+ c close a door+ d drop an object+ i display inventory+ o open a door+ s search for secret doors+ q quaff a potion+ M display previous messages+ S save and quit the game+ Q quit without saving+ . wait+ , pick up an object+ : look around+ < ascend a level+ > descend a level++One of the ways of moving throughout the level is with the vi text editor keys+(also known as "Rogue-like keys"):++ key command+ k up+ j down+ h left+ l right+ y up-left+ u up-right+ b down-left+ n down-right++Pressing a capital letter corresponding to a direction key will have+the hero run in that direction until something interesting occurs.++It's also possible to move using the numerical keypad, with Shift for running+and the middle '5' key for resting. (If you are using the curses frontend,+numerical keypad may not work correctly for terminals with broken terminfo,+e.g., gnome terminal tends to have problems, while xterm works fine.)++Below are also some debug and cheat keys. Use at your peril!++ key command+ v display the version of the game+ O toggle "omniscience"+ I display level meta-data+ R toggle smell display+ T toggle level generation sequence+ V toggle field of vision display+++Monsters+--------++The hero is not alone in the dungeon. Monsters roam the game world, too.+Monsters inhabit specific locations on the game map, and can be seen+if the tile they are on can be seen by the hero.+Every monster gets a turn per move of the hero. Monster moves+are restricted in the same way as hero moves, i.e., they cannot move+into obstacles like walls or rock. Some monsters+ignore the hero, others chase him only when they see him+and the especially dangerous kind is able to smell the hero.++When the hero moves into a monster or a monster bumps into the hero,+combat occurs. Whenever combat occurs, the attacked party may lose some health.+If the hero dies, the game ends.+++On Winning and Dying+--------------------++If you happen to die, you are free to start again from the first level+of the dungeon, but all your treasure is gone and the dungeon will look+different this time.++You win the game if you escape the dungeon alive with treasure and valuable+items --- the more the better!
− Perception.hs
@@ -1,48 +0,0 @@-module Perception where--import Data.Set as S--import Geometry-import State-import Level-import Monster-import FOV--data Perception =- Perception { preachable :: Set Loc, pvisible :: Set Loc }--perception_ :: State -> Level -> Perception-perception_ (State { splayer = Monster { mloc = ploc } }) (Level { lmap = lmap }) =- perception ploc lmap--perception :: Loc -> LMap -> Perception-perception ploc lmap =- let- -- This part is simple. "reachable" contains everything that is on an- -- unblocked path from the player position.- reachable = fullscan ploc lmap- -- In "actVisible", we store the locations that have light and are- -- reachable. Furthermore, the player location itself is always- -- visible.- actVisible = S.filter (\ loc -> light (lmap `at` loc)) reachable `S.union` S.singleton ploc- srnd = S.fromList $ surroundings ploc- -- In "dirVisible", we store locations in the surroundings that are- -- perceptible from the current position.- dirVisible = S.filter (\ loc -> let p = perceptible (lmap `at` loc) :: [Dir]- in any (\ d -> shift loc d == ploc) p)- (S.fromList $ surroundings ploc)- ownVisible = S.union actVisible dirVisible- -- Something is "pasVisible" if it is reachable passively visible from an- -- "actVisible" location, *or* if it is in the surroundings and passively- -- visible from a "dirVisible" location. (This is complicated, and I'd- -- like to simplify it, but for now, it seems to at least do what I- -- want.)- pasVisible = S.filter (\ loc -> let p = passive (lmap `at` loc)- dp = S.member loc srnd- s = if dp then ownVisible else actVisible- in any (\ d -> S.member (shift loc d) s) p)- reachable- visible = S.unions [pasVisible, actVisible, dirVisible]- in- Perception reachable visible-
− README
@@ -1,12 +0,0 @@--Installation via Cabal.--There are three displays to choose from, depending on-the flags passed to Setup configure:-- * gtk2hs (the default)- * vty (Setup configure -f-gtk)- * curses (Setup configure -fcurses)--The curses display is the newest and least tested,-the other two should be stable.
+ README.markdown view
@@ -0,0 +1,37 @@+LambdaHack+==========++LambdaHack is a small [roguelike] [1] game written in [Haskell] [2].+++Compilation and installation+----------------------------++The game is best compiled and installed via Cabal, which also takes care+of all dependencies. The latest official version of the game can be downloaded+automatically by Cabal from [Hackage] [3] as follows++ cabal install LambdaHack++For a more current snapshot, download the source from [github] [4]+and run Cabal from the main directory++ cabal install++or you may try one of the terminal frontends with++ cabal install -fvty+++Further information+-------------------++See files PLAYING.markdown, DESIGN.markdown, CREDITS and LICENSE+for more information.++++[1]: http://roguebasin.roguelikedevelopment.org/index.php?title=Berlin_Interpretation+[2]: http://www.haskell.org/+[3]: http://hackage.haskell.org/package/LambdaHack+[4]: http://github.com/kosmikus/LambdaHack
− Random.hs
@@ -1,67 +0,0 @@-module Random (module Frequency, module Random) where--import Data.Ratio-import qualified System.Random as R-import Control.Monad.State--import Frequency--type Rnd a = State R.StdGen a--randomR :: (R.Random a) => (a, a) -> Rnd a-randomR = State . R.randomR--binaryChoice :: a -> a -> Rnd a-binaryChoice p0 p1 =- do- b <- randomR (False,True)- return (if b then p0 else p1)--chance :: Rational -> Rnd Bool-chance r =- do- let n = numerator r- d = denominator r- k <- randomR (1,d)- return (k <= n)---- | d for die/dice-d :: Int -> Rnd Int-d x = randomR (1,x)--oneOf :: [a] -> Rnd a-oneOf xs =- do- r <- randomR (0, length xs - 1)- return (xs !! r)--frequency :: Frequency a -> Rnd a-frequency (Frequency xs) =- do- r <- randomR (1, sum (map fst xs))- return (frequency' r xs)- where- frequency' :: Int -> [(Int, a)] -> a- frequency' _ [(_, x)] = x- frequency' m ((n, x) : xs)- | m <= n = x- | otherwise = frequency' (m - n) xs--rndToIO :: Rnd a -> IO a-rndToIO r =- do- g <- R.getStdGen- let (x,g') = runState r g- R.setStdGen g'- return x---- ** Arithmetic operations on Rnd.--infixl 7 *~-infixl 6 ~+~--(~+~) :: Num a => Rnd a -> Rnd a -> Rnd a-(~+~) = liftM2 (+)--(*~) :: Num a => Int -> Rnd a -> Rnd a-x *~ r = liftM sum (replicateM x r)
− Save.hs
@@ -1,34 +0,0 @@-module Save where--import System.Directory-import Control.Exception as E hiding (handle)--import File-import Level-import State---- | Name of the save game. TODO: It should be possible to play--- multiple games next to each other (for instance, in a multi-user--- environment), so the savegames should contain the character name--- and the user id or something like that.-savefile = "LambdaHack.save"---- | We save a simple serialized version of the current level and--- the current state. The 'False' is used only as an EOF marker.-saveGame :: Level -> State -> IO ()-saveGame lvl state = encodeCompressedFile savefile (lvl,state,False)---- | Restore a saved game. Returns either the current level and--- game state, or a string containing an error message if restoring--- the game fails.-restoreGame :: IO (Either (Level, State) String)-restoreGame =- E.catch (do- r <- strictDecodeCompressedFile savefile- removeFile savefile- case r of- (x,y,z) -> (z :: Bool) `seq` return $ Left (x,y))- (\ e -> case e of- _ -> return (Right $ "Restore failed: " ++- (unwords . lines) (show e)))-
− State.hs
@@ -1,116 +0,0 @@-module State where--import qualified Data.Map as M-import qualified Data.Set as S-import Control.Monad-import Data.Binary--import Monster-import Geometry-import Level-import Item---- | The 'State' contains all the game state except the current dungeon--- level which we usually keep separate.-data State = State- { splayer :: Monster,- shistory :: [String],- ssensory :: SensoryMode,- sdisplay :: DisplayMode,- stime :: Time,- sassocs :: Assocs, -- ^ how does every item appear- sdiscoveries :: Discoveries, -- ^ items (types) have been discovered- sdungeon :: Dungeon -- ^ all but current dungeon level- }- deriving Show--defaultState ploc dng =- State- (defaultPlayer ploc)- []- Implicit Normal- 0- M.empty- S.empty- dng--updatePlayer :: State -> (Monster -> Monster) -> State-updatePlayer s f = s { splayer = f (splayer s) }--updateHistory :: State -> ([String] -> [String]) -> State-updateHistory s f = s { shistory = f (shistory s) }--updateDiscoveries :: State -> (Discoveries -> Discoveries) -> State-updateDiscoveries s f = s { sdiscoveries = f (sdiscoveries s) }--toggleVision :: State -> State-toggleVision s = s { ssensory = if ssensory s == Vision then Implicit else Vision }--toggleSmell :: State -> State-toggleSmell s = s { ssensory = if ssensory s == Smell then Implicit else Smell }--toggleOmniscient :: State -> State-toggleOmniscient s = s { sdisplay = if sdisplay s == Omniscient then Normal else Omniscient }--toggleTerrain :: State -> State-toggleTerrain s = s { sdisplay = case sdisplay s of Terrain 1 -> Normal; Terrain n -> Terrain (n-1); _ -> Terrain 4 }--instance Binary State where- put (State player hst sense disp time assocs discs dng) =- do- put player- put hst- put sense- put disp- put time- put assocs- put discs- put dng- get =- do- player <- get- hst <- get- sense <- get- disp <- get- time <- get- assocs <- get- discs <- get- dng <- get- return (State player hst sense disp time assocs discs dng)--data SensoryMode =- Implicit- | Vision- | Smell- deriving (Show, Eq)--instance Binary SensoryMode where- put Implicit = putWord8 0- put Vision = putWord8 1- put Smell = putWord8 2- get = do- tag <- getWord8- case tag of- 0 -> return Implicit- 1 -> return Vision- 2 -> return Smell- _ -> fail "no parse (SensoryMode)"--data DisplayMode =- Normal- | Omniscient- | Terrain Int- deriving (Show, Eq)--instance Binary DisplayMode where- put Normal = putWord8 0- put Omniscient = putWord8 1- put (Terrain n) = putWord8 2 >> put n- get = do- tag <- getWord8- case tag of- 0 -> return Normal- 1 -> return Omniscient- 2 -> liftM Terrain get- _ -> fail "no parse (DisplayMode)"-
− Strategy.hs
@@ -1,49 +0,0 @@-module Strategy where--import Control.Monad--import Frequency---- Monster strategies---- | A strategy is a choice of frequency tables.-newtype Strategy a = Strategy { runStrategy :: [Frequency a] }- deriving Show---- | Strategy is a monad. TODO: Can we write this as a monad transformer?-instance Monad Strategy where- return x = Strategy $ return (Frequency [(1, x)])- m >>= f = Strategy $- filter (\ (Frequency xs) -> not (null xs))- [ Frequency [ (p * q, b) - | (p, a) <- runFrequency x,- y <- runStrategy (f a),- (q, b) <- runFrequency y ] - | x <- runStrategy m ]--liftFrequency :: Frequency a -> Strategy a-liftFrequency f = Strategy [f]--instance MonadPlus Strategy where- mzero = Strategy []- mplus (Strategy xs) (Strategy ys) = Strategy (xs ++ ys)--infixr 2 .|--(.|) :: Strategy a -> Strategy a -> Strategy a-(.|) = mplus--reject :: Strategy a-reject = mzero--infix 3 .=>--(.=>) :: Bool -> Strategy a -> Strategy a-p .=> m | p = m- | otherwise = mzero--only :: (a -> Bool) -> Strategy a -> Strategy a-only p s =- do- x <- s- p x .=> return x
− Style.hs
@@ -1,355 +0,0 @@--- --- Copyright (c) 2004-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons--- --- This program is free software; you can redistribute it and/or--- modify it under the terms of the GNU General Public License as--- published by the Free Software Foundation; either version 2 of--- the License, or (at your option) any later version.--- --- This program is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU--- General Public License for more details.--- --- You should have received a copy of the GNU General Public License--- along with this program; if not, write to the Free Software--- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA--- 02111-1307, USA.--- ------- | Color manipulation-----module Style where--#include "config.h"--import qualified Curses-import Data.ByteString (ByteString)--import Data.Char (toLower)-import Data.Word (Word8)-import Data.Maybe (fromJust)-import Data.IORef (readIORef, writeIORef, newIORef, IORef)-import qualified Data.Map as M (fromList, empty, lookup, Map)--import System.IO.Unsafe (unsafePerformIO)-import Control.Exception (handle)------------------------------------------------------------------------------ | User-configurable colours--- Each component of this structure corresponds to a fg\/bg colour pair--- for an item in the ui-data UIStyle = UIStyle {- window :: !Style -- default window colour- , helpscreen :: !Style -- help screen- , titlebar :: !Style -- titlebar of window- , selected :: !Style -- currently playing track- , cursors :: !Style -- the scrolling cursor line- , combined :: !Style -- the style to use when the cursor is on the current track- , warnings :: !Style -- style for warnings- , blockcursor :: !Style -- style for the block cursor when typing text- , progress :: !Style -- style for the progress bar- }------------------------------------------------------------------------------ | Colors -data Color- = RGB {-# UNPACK #-} !Word8 !Word8 !Word8- | Default- | Reverse- deriving (Eq,Ord)---- | Foreground and background color pairs-data Style = Style {-# UNPACK #-} !Color !Color - deriving (Eq,Ord)---- | A list of such values (the representation is optimised)-data StringA - = Fast {-# UNPACK #-} !ByteString !Style- | FancyS {-# UNPACK #-} ![(ByteString,Style)] -- one line made up of segments-------------------------------------------------------------------------------- | Some simple colours (derivied from proxima\/src\/common\/CommonTypes.hs)------ But we don't have a light blue?----black, grey, darkred, red, darkgreen, green, brown, yellow :: Color-darkblue, blue, purple, magenta, darkcyan, cyan, white, brightwhite :: Color-black = RGB 0 0 0-grey = RGB 128 128 128-darkred = RGB 139 0 0-red = RGB 255 0 0-darkgreen = RGB 0 100 0-green = RGB 0 128 0-brown = RGB 165 42 42-yellow = RGB 255 255 0-darkblue = RGB 0 0 139-blue = RGB 0 0 255-purple = RGB 128 0 128-magenta = RGB 255 0 255-darkcyan = RGB 0 139 139 -cyan = RGB 0 255 255-white = RGB 165 165 165-brightwhite = RGB 255 255 255--defaultfg, defaultbg, reversefg, reversebg :: Color-#if defined(HAVE_USE_DEFAULT_COLORS)-defaultfg = Default-defaultbg = Default-#else-defaultfg = white-defaultbg = black-#endif-reversefg = Reverse-reversebg = Reverse------- | map strings to colors----stringToColor :: String -> Maybe Color-stringToColor s = case map toLower s of- "black" -> Just black- "grey" -> Just grey- "darkred" -> Just darkred- "red" -> Just red- "darkgreen" -> Just darkgreen- "green" -> Just green- "brown" -> Just brown- "yellow" -> Just yellow- "darkblue" -> Just darkblue- "blue" -> Just blue- "purple" -> Just purple- "magenta" -> Just magenta- "darkcyan" -> Just darkcyan- "cyan" -> Just cyan- "white" -> Just white- "brightwhite" -> Just brightwhite- "default" -> Just Default- "reverse" -> Just Reverse- _ -> Nothing-------------------------------------------------------------------------------- | Set some colours, perform an action, and then reset the colours----withStyle :: Style -> (IO ()) -> IO ()-withStyle sty fn = uiAttr sty >>= setAttribute >> fn >> reset-{-# INLINE withStyle #-}------- | manipulate the current attributes of the standard screen--- Only set attr if it's different to the current one?----setAttribute :: (Curses.Attr, Curses.Pair) -> IO ()-setAttribute = uncurry Curses.attrSet-{-# INLINE setAttribute #-}------- | Reset the screen to normal values----reset :: IO ()-reset = setAttribute (Curses.attr0, Curses.Pair 0)-{-# INLINE reset #-}------- | And turn on the colours----initcolours :: UIStyle -> IO ()-initcolours sty = do- let ls = [helpscreen sty, warnings sty, window sty, - selected sty, titlebar sty, progress sty,- blockcursor sty, cursors sty, combined sty ]- (Style fg bg) = progress sty -- bonus style-- pairs <- initUiColors (ls ++ [Style bg bg, Style fg fg])- writeIORef pairMap pairs- -- set the background- uiAttr (window sty) >>= \(_,p) -> Curses.bkgrndSet nullA p-------------------------------------------------------------------------------- | Set up the ui attributes, given a ui style record------ Returns an association list of pairs for foreground and bg colors,--- associated with the terminal color pair that has been defined for--- those colors.----initUiColors :: [Style] -> IO PairMap-initUiColors stys = do - ls <- sequence [ uncurry fn m | m <- zip stys [1..] ]- return (M.fromList ls)- where- fn :: Style -> Int -> IO (Style, (Curses.Attr,Curses.Pair))- fn sty p = do- let (CColor (a,fgc),CColor (b,bgc)) = style2curses sty- handle (\_ -> return ()) $ Curses.initPair (Curses.Pair p) fgc bgc- return (sty, (a `Curses.attrPlus` b, Curses.Pair p))-------------------------------------------------------------------------------- | Getting from nice abstract colours to ncurses-settable values---- 20% of allocss occur here! But there's only 3 or 4 colours :/--- Every call to uiAttr----uiAttr :: Style -> IO (Curses.Attr, Curses.Pair)-uiAttr sty = do- m <- readIORef pairMap- return $ lookupPair m sty-{-# INLINE uiAttr #-}---- | Given a curses color pair, find the Curses.Pair (i.e. the pair--- curses thinks these colors map to) from the state-lookupPair :: PairMap -> Style -> (Curses.Attr, Curses.Pair)-lookupPair m s = case M.lookup s m of- Nothing -> (Curses.attr0, Curses.Pair 0) -- default settings- Just v -> v-{-# INLINE lookupPair #-}---- | Keep a map of nice style defs to underlying curses pairs, created at init time-type PairMap = M.Map Style (Curses.Attr, Curses.Pair)---- | map of Curses.Color pairs to ncurses terminal Pair settings-pairMap :: IORef PairMap-pairMap = unsafePerformIO $ newIORef M.empty-{-# NOINLINE pairMap #-}-------------------------------------------------------------------------------- Basic (ncurses) colours.----defaultColor :: Curses.Color-defaultColor = fromJust $ Curses.color "default"--cblack, cred, cgreen, cyellow, cblue, cmagenta, ccyan, cwhite :: Curses.Color-cblack = fromJust $ Curses.color "black"-cred = fromJust $ Curses.color "red"-cgreen = fromJust $ Curses.color "green"-cyellow = fromJust $ Curses.color "yellow"-cblue = fromJust $ Curses.color "blue"-cmagenta = fromJust $ Curses.color "magenta"-ccyan = fromJust $ Curses.color "cyan"-cwhite = fromJust $ Curses.color "white"------- Combine attribute with another attribute----setBoldA, setReverseA :: Curses.Attr -> Curses.Attr-setBoldA = flip Curses.setBold True-setReverseA = flip Curses.setReverse True------- | Some attribute constants----boldA, nullA, reverseA :: Curses.Attr-nullA = Curses.attr0-boldA = setBoldA nullA-reverseA = setReverseA nullA----------------------------------------------------------------------------newtype CColor = CColor (Curses.Attr, Curses.Color)--- --- | Map Style rgb rgb colours to ncurses pairs--- TODO a generic way to turn an rgb into the nearest curses color----style2curses :: Style -> (CColor, CColor)-style2curses (Style fg bg) = (fgCursCol fg, bgCursCol bg)-{-# INLINE style2curses #-}--fgCursCol :: Color -> CColor-fgCursCol c = case c of- RGB 0 0 0 -> CColor (nullA, cblack)- RGB 128 128 128 -> CColor (boldA, cblack)- RGB 139 0 0 -> CColor (nullA, cred)- RGB 255 0 0 -> CColor (boldA, cred)- RGB 0 100 0 -> CColor (nullA, cgreen)- RGB 0 128 0 -> CColor (boldA, cgreen)- RGB 165 42 42 -> CColor (nullA, cyellow)- RGB 255 255 0 -> CColor (boldA, cyellow)- RGB 0 0 139 -> CColor (nullA, cblue)- RGB 0 0 255 -> CColor (boldA, cblue)- RGB 128 0 128 -> CColor (nullA, cmagenta)- RGB 255 0 255 -> CColor (boldA, cmagenta)- RGB 0 139 139 -> CColor (nullA, ccyan)- RGB 0 255 255 -> CColor (boldA, ccyan)- RGB 165 165 165 -> CColor (nullA, cwhite)- RGB 255 255 255 -> CColor (boldA, cwhite)- Default -> CColor (nullA, defaultColor)- Reverse -> CColor (reverseA, defaultColor)- _ -> CColor (nullA, cblack) -- NB--bgCursCol :: Color -> CColor-bgCursCol c = case c of- RGB 0 0 0 -> CColor (nullA, cblack)- RGB 128 128 128 -> CColor (nullA, cblack)- RGB 139 0 0 -> CColor (nullA, cred)- RGB 255 0 0 -> CColor (nullA, cred)- RGB 0 100 0 -> CColor (nullA, cgreen)- RGB 0 128 0 -> CColor (nullA, cgreen)- RGB 165 42 42 -> CColor (nullA, cyellow)- RGB 255 255 0 -> CColor (nullA, cyellow)- RGB 0 0 139 -> CColor (nullA, cblue)- RGB 0 0 255 -> CColor (nullA, cblue)- RGB 128 0 128 -> CColor (nullA, cmagenta)- RGB 255 0 255 -> CColor (nullA, cmagenta)- RGB 0 139 139 -> CColor (nullA, ccyan)- RGB 0 255 255 -> CColor (nullA, ccyan)- RGB 165 165 165 -> CColor (nullA, cwhite)- RGB 255 255 255 -> CColor (nullA, cwhite)- Default -> CColor (nullA, defaultColor)- Reverse -> CColor (reverseA, defaultColor)- _ -> CColor (nullA, cwhite) -- NB--defaultSty :: Style-defaultSty = Style Default Default-------------------------------------------------------------------------------- Support for runtime configuration--- We choose a simple strategy, read/showable record types, with strings--- to represent colors------ The fields must map to UIStyle------ It is this data type that is stored in 'show' format in ~/.hmp3----data Config = Config {- hmp3_window :: (String,String)- , hmp3_helpscreen :: (String,String)- , hmp3_titlebar :: (String,String)- , hmp3_selected :: (String,String)- , hmp3_cursors :: (String,String)- , hmp3_combined :: (String,String)- , hmp3_warnings :: (String,String)- , hmp3_blockcursor :: (String,String)- , hmp3_progress :: (String,String)- } deriving (Show,Read)------- | Read the ~/.hmp3 file, and construct a UIStyle from it, to insert--- into ----buildStyle :: Config -> UIStyle-buildStyle bs = UIStyle {- window = f $ hmp3_window bs- , helpscreen = f $ hmp3_helpscreen bs- , titlebar = f $ hmp3_titlebar bs- , selected = f $ hmp3_selected bs- , cursors = f $ hmp3_cursors bs- , combined = f $ hmp3_combined bs- , warnings = f $ hmp3_warnings bs- , blockcursor = f $ hmp3_blockcursor bs- , progress = f $ hmp3_progress bs- }-- where - f (x,y) = Style (g x) (g y)- g x = case stringToColor x of- Nothing -> Default- Just y -> y
− Turn.hs
@@ -1,621 +0,0 @@-module Turn where--import Data.List as L-import Data.Map as M-import Data.Set as S-import Data.Char-import Data.Maybe-import Data.Function--import State-import Geometry-import Level-import Dungeon-import Monster-import Actor-import Perception-import Item-import Display2-import Random-import Save-import Message-import Version-import Strategy---- | Perform a complete turn (i.e., monster moves etc.)-loop :: Session -> Level -> State -> String -> IO ()-loop session (lvl@(Level nm sz ms smap lmap lmeta))- (state@(State { splayer = player@(Monster { mhp = php, mloc = ploc }), stime = time }))- oldmsg =- do- -- update smap- let nsmap = M.insert ploc (time + smellTimeout) smap- -- determine player perception- let per = perception ploc lmap- -- perform monster moves- handleMonsters session (lvl { lsmell = nsmap }) state per oldmsg---- | Handle monster moves. The idea is that we perform moves--- as long as there are monsters that have a move time which is--- less than or equal to the current time.-handleMonsters :: Session -> Level -> State -> Perception -> String -> IO ()-handleMonsters session lvl@(Level { lmonsters = ms })- (state@(State { stime = time }))- per oldmsg =- -- for debugging: causes redraw of the current state for every monster move; slow!- -- displayLevel session lvl per state oldmsg >>- case ms of- [] -> -- there are no monsters, just continue- handlePlayer- (m@(Monster { mtime = mt }) : ms)- | mt > time -> -- all the monsters are not yet ready for another move,- -- so continue- handlePlayer- | mhp m <= 0 -> -- the monster dies- handleMonsters session (updateMonsters lvl (const ms))- state per oldmsg- | otherwise -> -- monster m should move- handleMonster m session (updateMonsters lvl (const ms))- state per oldmsg- where- nstate = state { stime = time + 1 }-- -- good place to do everything that has to be done for every *time*- -- unit; currently, that's monster generation- handlePlayer =- do- nlvl <- rndToIO (addMonster lvl (splayer nstate))- handle session nlvl nstate per oldmsg- ---- | Handle the move of a single monster.-handleMonster :: Monster -> Session -> Level -> State -> Perception -> String ->- IO ()-handleMonster m session lvl@(Level { lmonsters = ms, lsmell = nsmap, lmap = lmap })- (state@(State { splayer = player@(Monster { mloc = ploc }), stime = time }))- per oldmsg =- do- nl <- rndToIO (frequency (head (runStrategy (strategy m lvl state per .| wait))))-- -- increase the monster move time and set direction- let nm = m { mtime = time + mspeed m, mdir = if nl == (0,0) then Nothing else Just nl }- let (act, nms) = insertMonster nm ms- let nlvl = updateMonsters lvl (const nms)- moveOrAttack- True- (\ nlvl np msg ->- handleMonsters session nlvl (updatePlayer state (const np)) per- (addMsg oldmsg msg))- (handleMonsters session nlvl state per oldmsg)- nlvl player (sassocs state) (sdiscoveries state) per- (AMonster act)- nl--strategy :: Monster -> Level -> State -> Perception -> Strategy Loc-strategy m@(Monster { mtype = mt, mloc = me, mdir = mdir })- lvl@(Level { lmonsters = ms, lsmell = nsmap, lmap = lmap })- (state@(State { splayer = player@(Monster { mloc = ploc }), stime = time }))- per =- case mt of- Eye -> eye- FastEye -> eye- Nose -> nose - _ -> onlyAccessible moveRandomly- where- -- we check if the monster is visible by the player rather than if the- -- player is visible by the monster -- this is more efficient, but- -- won't be correct in the general situation- playerVisible = me `S.member` pvisible per- playerAdjacent = adjacent me ploc- towardsPlayer = towards (me, ploc)- onlyTowardsPlayer = only (\ x -> distance (towardsPlayer, x) <= 1)- onlyPreservesDir = only (\ x -> maybe True (\ d -> distance (neg d, x) > 1) mdir) - onlyUnoccupied = onlyMoves (unoccupied ms lmap) me- onlyAccessible = onlyMoves (accessible lmap me) me- smells = L.map fst $- L.sortBy (\ (_,s1) (_,s2) -> compare s2 s1) $- L.filter (\ (_,s) -> s > 0) $ - L.map (\ x -> (x, nsmap ! (me `shift` x) - time `max` 0)) moves-- eye = playerAdjacent .=> return towardsPlayer- .| (onlyUnoccupied $ onlyAccessible $- playerVisible .=> onlyTowardsPlayer moveRandomly- .| onlyPreservesDir moveRandomly)-- nose = playerAdjacent .=> return towardsPlayer- .| (onlyAccessible $- foldr (.|) reject (L.map return smells)- .| moveRandomly)--onlyMoves :: (Dir -> Bool) -> Loc -> Strategy Dir -> Strategy Dir-onlyMoves p l = only (\ x -> p (l `shift` x))--moveRandomly :: Strategy Dir-moveRandomly = liftFrequency $ uniform moves--wait :: Strategy Dir-wait = return (0,0)---- | Display current status and handle the turn of the player.-handle :: Session -> Level -> State -> Perception -> String -> IO ()-handle session (lvl@(Level nm sz ms smap lmap lmeta))- (state@(State { splayer = player@(Monster { mhp = php, mdir = pdir, mloc = ploc, mitems = pinv, mtime = ptime }), stime = time, sassocs = assocs, sdiscoveries = discs }))- per oldmsg =- -- check for player death- if php <= 0- then do- displayCurrent (addMsg oldmsg ("You die ..." ++ more))- getConfirm session- shutdown session- else -- check if the player can make another move yet- if ptime > time then- do- -- do not make intermediate redraws while running- maybe (displayLevel session lvl per state "") (const $ return ()) pdir- handleMonsters session lvl state per oldmsg - -- NOTE: It's important to call handleMonsters here, not loop,- -- because loop does all sorts of calculations that are only- -- really necessary after the player has moved.- else do- displayCurrent oldmsg- let h = nextEvent session >>= h'- h' e =- handleDirection e (move h) $ - handleDirection (L.map toLower e) run $- handleModifier e h $- case e of- "o" -> openclose True h- "c" -> openclose False h- "s" -> search h-- "less" -> lvlchange Up h- "greater" -> lvlchange Down h-- -- items- "comma" -> pickup h- "d" -> drop h- "i" -> inventory h- "q" -> drink h-- -- saving or ending the game- "S" -> saveGame lvl mstate >> shutdown session- "Q" -> shutdown session- "Escape" -> displayCurrent "Press Q to quit." >> h-- -- wait- "space" -> loop session nlvl nstate ""- "period" -> loop session nlvl nstate ""-- -- look- "colon" -> lookAround h-- -- display modes- "V" -> handle session nlvl (toggleVision state) per oldmsg- "R" -> handle session nlvl (toggleSmell state) per oldmsg- "O" -> handle session nlvl (toggleOmniscient state) per oldmsg- "T" -> handle session nlvl (toggleTerrain state) per oldmsg-- -- meta information- "M" -> displayCurrent' "" (unlines (shistory mstate) ++ more) >>= \ b ->- if b then getOptionalConfirm session- (const (displayCurrent "" >> h)) h'- else displayCurrent "" >> h- "I" -> displayCurrent lmeta >> h- "v" -> displayCurrent version >> h-- s -> displayCurrent ("unknown command (" ++ s ++ ")") >> h- maybe h continueRun pdir-- where-- -- we record the oldmsg in the history- mstate = if L.null oldmsg then state else updateHistory state (take 500 . ((oldmsg ++ " "):))- -- TODO: make history max configurable-- reachable = preachable per- visible = pvisible per-- displayCurrent :: String -> IO ()- displayCurrent = displayLevel session nlvl per state-- displayCurrent' :: String -> String -> IO Bool- displayCurrent' = displayOverlay session nlvl per state-- -- update player memory- nlmap = foldr (\ x m -> M.update (\ (t,_) -> Just (t,t)) x m) lmap (S.toList visible)- nlvl = updateLMap lvl (const nlmap)-- -- update player action time, and regenerate hitpoints- -- player HP regeneration, TODO: remove hardcoded max and time interval- nplayer = player { mtime = time + mspeed player,- mhp = if time `mod` 1500 == 0 then (php + 1) `min` playerHP else php }- nstate = updatePlayer mstate (const nplayer)-- -- picking up items- pickup abort = pickupItem- displayCurrent- (\ l p -> loop session l (updatePlayer nstate (const p)))- abort- nlvl nplayer assocs discs-- -- dropping items- drop abort = dropItem- session- displayCurrent- displayCurrent'- (\ l p -> loop session l (updatePlayer nstate (const p)))- abort- nlvl nplayer assocs discs-- -- drinking potions- drink abort = drinkPotion- session- displayCurrent- displayCurrent'- (\ l p d -> loop session l- (updateDiscoveries (updatePlayer nstate (const p)) (S.union d)))- abort- nlvl nplayer assocs discs- - -- display inventory- inventory abort - | L.null (mitems player) =- displayCurrent "You are not carrying anything." >> abort- | otherwise =- do- displayItems displayCurrent' assocs discs - "This is what you are carrying:" True (mitems player)- getConfirm session- displayCurrent ""- abort -- looking at inventory doesn't take any time- - -- look around at current location- lookAround abort =- do- -- check if something is here to pick up- let t = nlmap `at` ploc- if length (titems t) <= 2- then displayCurrent (lookAt True assocs discs nlmap ploc) >> abort- else- do- displayItems displayCurrent' assocs discs- (lookAt True assocs discs nlmap ploc) False (titems t)- getConfirm session- displayCurrent ""- abort -- looking around doesn't take any time-- -- open and close doors- openclose o abort =- do- displayCurrent "direction?"- e <- nextEvent session- handleDirection e (openclose' o abort) (displayCurrent "never mind" >> abort)- openclose' o abort dir =- let txt = if o then "open" else "closed"- dloc = shift ploc dir- in- case nlmap `at` dloc of- Tile d@(Door hv o') is- | secret o' -> displayCurrent "never mind" >> abort- | toOpen (not o) /= o'- -> displayCurrent ("already " ++ txt) >> abort- | not (unoccupied ms nlmap dloc)- -> displayCurrent "blocked" >> abort- | otherwise -> -- ok, we can open/close the door - let nt = Tile (Door hv (toOpen o)) is- clmap = M.insert (shift ploc dir) (nt, nt) nlmap- in loop session (updateLMap lvl (const clmap)) nstate ""- _ -> displayCurrent "never mind" >> abort- -- search for secret doors- search abort =- let searchTile (Tile (Door hv (Just n)) x,t') = Just (Tile (Door hv (Just (max (n - 1) 0))) x, t')- searchTile t = Just t- slmap = foldl (\ l m -> update searchTile (shift ploc m) l) nlmap moves- in loop session (updateLMap lvl (const slmap)) nstate ""- -- perform a level change- lvlchange vdir abort =- case nlmap `at` ploc of- Tile (Stairs _ vdir' next) is- | vdir == vdir' -> -- ok- case next of- Nothing -> -- exit dungeon- shutdown session- Just (nln, nloc) ->- -- perform level change- do- -- put back current level- -- (first put back, then get, in case we change to the same level!)- let full = putDungeonLevel lvl (sdungeon nstate)- -- get new level- (new, ndng) = getDungeonLevel nln full- lstate = nstate { sdungeon = ndng }- loop session new (updatePlayer lstate (const (player { mloc = nloc }))) ""- _ -> -- no stairs- let txt = if vdir == Up then "up" else "down" in- displayCurrent ("no stairs " ++ txt) >> abort- -- run into a direction- run dir =- do- let mplayer = nplayer { mdir = Just dir }- abort = handle session nlvl (updatePlayer state (const $ player { mdir = Nothing })) per ""- moveOrAttack- False -- attacks are disallowed while running- (\ l p -> loop session l (updatePlayer nstate (const p)))- abort- nlvl mplayer assocs discs per APlayer dir- continueRun dir =- let abort = handle session nlvl (updatePlayer state (const $ player { mdir = Nothing })) per oldmsg- dloc = shift ploc dir- in case (oldmsg, nlmap `at` ploc) of- (_:_, _) -> abort- (_, Tile (Opening {}) _) -> abort- (_, Tile (Door {}) _) -> abort- (_, Tile (Stairs {}) _) -> abort- _- | accessible nlmap ploc dloc ->- moveOrAttack- False -- attacks are disallowed while running- (\ l p -> loop session l (updatePlayer nstate (const p)))- abort- nlvl nplayer assocs discs per APlayer dir- (_, Tile Corridor _) -- direction change restricted to corridors- | otherwise ->- let ns = L.filter (\ x -> distance (neg dir,x) > 1- && accessible nlmap ploc (ploc `shift` x)) moves- sns = L.filter (\ x -> distance (dir,x) <= 1) ns- in case ns of- [newdir] -> run newdir- _ -> case sns of- [newdir] -> run newdir- _ -> abort- _ -> abort- -- perform a player move- move abort dir = moveOrAttack- True -- attacks are allowed- (\ l p -> loop session l (updatePlayer nstate (const p)))- abort- nlvl nplayer assocs discs per APlayer dir---- | Drinking potions.-drinkPotion :: Session -> -- session- (String -> IO ()) -> -- how to display- (String -> String -> IO Bool) -> -- overlay display- (Level -> Player -> Discoveries -> String -> IO a) -> - -- success continuation- IO a -> -- failure continuation- Level -> -- the level- Player -> -- the player- Assocs ->- Discoveries ->- IO a-drinkPotion session displayCurrent displayCurrent' continue abort- nlvl@(Level { lmap = nlmap }) nplayer@(Monster { mloc = ploc }) assocs discs- | L.null (mitems nplayer) =- displayCurrent "You are not carrying anything." >> abort- | otherwise =- do- i <- getPotions session displayCurrent displayCurrent' assocs discs- "What to drink?" (mitems nplayer)- case i of- Just i'@(Item { itype = Potion ptype }) ->- let iplayer = nplayer { mitems = deleteBy ((==) `on` iletter) i' (mitems nplayer) }- t = nlmap `at` ploc- msg = subjectMonster (mtype nplayer) ++ " " ++- verbMonster (mtype nplayer) "drink" ++ " " ++- objectItem assocs discs (icount i') (itype i') ++ ". " ++- pmsg ptype- pmsg PotionWater = "Tastes like water."- pmsg PotionHealing = "You feel better."- fplayer PotionWater = iplayer- fplayer PotionHealing = iplayer { mhp = 20 }- in continue nlvl- (fplayer ptype) (S.singleton (itype i')) msg- Just _ -> displayCurrent "you cannot drink that" >> abort- Nothing -> displayCurrent "never mind" >> abort----- | Dropping items.-dropItem :: Session -> -- session- (String -> IO ()) -> -- how to display- (String -> String -> IO Bool) -> -- overlay display- (Level -> Player -> String -> IO a) -> -- success continuation- IO a -> -- failure continuation- Level -> -- the level- Player -> -- the player- Assocs ->- Discoveries ->- IO a-dropItem session displayCurrent displayCurrent' continue abort- nlvl@(Level { lmap = nlmap }) nplayer@(Monster { mloc = ploc }) assocs discs- | L.null (mitems nplayer) =- displayCurrent "You are not carrying anything." >> abort- | otherwise =- do- i <- getAnyItem session displayCurrent displayCurrent' assocs discs- "What to drop?" (mitems nplayer)- case i of- Just i' -> let iplayer = nplayer { mitems = deleteBy ((==) `on` iletter) i' (mitems nplayer) }- t = nlmap `at` ploc- nt = t { titems = snd (joinItem i' (titems t)) }- plmap = M.insert ploc (nt, nt) nlmap- msg = subjectMonster (mtype nplayer) ++ " " ++- verbMonster (mtype nplayer) "drop" ++ " " ++- objectItem assocs discs (icount i') (itype i') ++ "."- in continue (updateLMap nlvl (const plmap))- iplayer msg- Nothing -> displayCurrent "never mind" >> abort------- | Picking up items.-pickupItem :: (String -> IO ()) -> -- how to display- (Level -> Player -> String -> IO a) -> -- success continuation- IO a -> -- failure continuation- Level -> -- the level- Player -> -- the player- Assocs ->- Discoveries ->- IO a-pickupItem displayCurrent continue abort- nlvl@(Level { lmap = nlmap }) nplayer@(Monster { mloc = ploc }) assocs discs =- do- -- check if something is here to pick up- let t = nlmap `at` ploc- case titems t of- [] -> displayCurrent "nothing here" >> abort- (i:rs) -> - case assignLetter (iletter i) (mletter nplayer) (mitems nplayer) of- Just l ->- let msg = -- (complete sentence, more adequate for monsters)- {-- subjectMonster (mtype player) ++ " " ++- compoundVerbMonster (mtype player) "pick" "up" ++ " " ++- objectItem (icount i) (itype i) ++ "."- -}- letterLabel (iletter ni) ++ objectItem assocs discs (icount ni) (itype ni)- nt = t { titems = rs }- plmap = M.insert ploc (nt, nt) nlmap- (ni,nitems) = joinItem (i { iletter = Just l }) (mitems nplayer)- iplayer = nplayer { mitems = nitems,- mletter = maxLetter l (mletter nplayer) }- in continue (updateLMap nlvl (const plmap))- iplayer msg- Nothing -> displayCurrent "cannot carry anymore" >> abort--getPotions :: Session ->- (String -> IO ()) ->- (String -> String -> IO Bool) ->- Assocs ->- Discoveries ->- String ->- [Item] ->- IO (Maybe Item)-getPotions session displayCurrent displayCurrent' assocs discs prompt is =- getItem session displayCurrent displayCurrent' assocs discs- prompt (\ i -> case itype i of Potion {} -> True; _ -> False)- "Potions in your inventory:" is--getAnyItem :: Session ->- (String -> IO ()) ->- (String -> String -> IO Bool) ->- Assocs ->- Discoveries ->- String ->- [Item] ->- IO (Maybe Item)-getAnyItem session displayCurrent displayCurrent' assocs discs prompt is =- getItem session displayCurrent displayCurrent' assocs discs- prompt (const True) "Objects in your inventory:" is--getItem :: Session ->- (String -> IO ()) ->- (String -> String -> IO Bool) ->- Assocs ->- Discoveries ->- String ->- (Item -> Bool) ->- String ->- [Item] ->- IO (Maybe Item) -getItem session displayCurrent displayCurrent' assocs discs prompt p ptext is0 =- let is = L.filter p is0- choice | L.null is = "[*]"- | otherwise = "[" ++ letterRange (concatMap (maybeToList . iletter) is) ++ " or ?*]"- r = do- displayCurrent (prompt ++ " " ++ choice)- let h = nextEvent session >>= h'- h' e =- handleModifier e h $- case e of- "question" -> do- b <- displayItems displayCurrent' assocs discs- ptext True is- if b then getOptionalConfirm session (const r) h'- else r - "asterisk" -> do- b <- displayItems displayCurrent' assocs discs- "Objects in your inventory:" True is0- if b then getOptionalConfirm session (const r) h'- else r- [l] -> return (find (\ i -> maybe False (== l) (iletter i)) is0)- _ -> return Nothing- h- in r--displayItems displayCurrent' assocs discs msg sorted is =- do- let inv = unlines $- L.map (\ (Item { icount = c, iletter = l, itype = t }) -> - letterLabel l ++ objectItem assocs discs c t ++ " ")- ((if sorted then sortBy (cmpLetter' `on` iletter) else id) is)- let ovl = inv ++ more- displayCurrent' msg ovl---moveOrAttack :: Bool -> -- allow attacks?- (Level -> Player -> String -> IO a) -> -- success continuation- IO a -> -- failure continuation- Level -> -- the level- Player -> -- the player- Assocs ->- Discoveries ->- Perception -> -- perception of the player- Actor -> -- who's moving?- Dir -> IO a-moveOrAttack allowAttacks- continue abort- nlvl@(Level { lmap = nlmap }) player assocs discs per- actor dir- -- to prevent monsters from hitting themselves- | dir == (0,0) = continue nlvl player ""- -- At the moment, we check whether there is a monster before checking accessibility- -- i.e., we can attack a monster on a blocked location. For instance,- -- a monster on an open door can be attacked diagonally, and a- -- monster capable of moving through walls can be attacked from an- -- adjacent position.- | not (L.null attacked) =- if allowAttacks then- do- let damage m = case mhp m of- 1 -> m { mhp = 0, mtime = 0 } -- grant an immediate move to die- h -> m { mhp = h - 1 }- let combatVerb m- | mhp m > 0 = "hit"- | otherwise = "kill"- let combatMsg m = subjectMonster (mtype am) ++ " " ++- verbMonster (mtype am) (combatVerb m) ++ " " ++- objectMonster (mtype m) ++ "."- let perceivedMsg m- | mloc m `S.member` pvisible per = combatMsg m- | otherwise = "You hear some noises."- let sortmtime = sortBy (\ x y -> compare (mtime x) (mtime y))- let updateVictims l p msg (a:r) =- updateActor damage (\ m l p -> updateVictims l p- (addMsg msg (perceivedMsg m)) r)- a l p- updateVictims l p msg [] = continue l {- (updateMonsters l sortmtime) -} p msg- updateVictims nlvl player "" attacked- else- abort- -- Perform a move.- | accessible nlmap aloc naloc = - updateActor (\ m -> m { mloc = naloc })- (\ _ l p -> continue l p (if actor == APlayer- then lookAt False assocs discs nlmap naloc else ""))- actor nlvl player- | otherwise = abort- where am :: Monster- am = getActor nlvl player actor- aloc :: Loc- aloc = mloc am- source = nlmap `at` aloc- naloc = shift aloc dir- target = nlmap `at` naloc- attackedPlayer = [ APlayer | mloc player == naloc ]- attackedMonsters = L.map AMonster $- findIndices (\ m -> mloc m == naloc) (lmonsters nlvl)- attacked :: [Actor]- attacked = attackedPlayer ++ attackedMonsters--
− Version.hs
@@ -1,12 +0,0 @@-module Version where--import Data.Version---- Cabal-import qualified Paths_LambdaHack as Self (version)--import Display--version :: String-version = showVersion Self.version ++ " (" ++ displayId ++ " frontend)"-
+ src/Action.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE MultiParamTypeClasses, RankNTypes #-}+module Action where++import Control.Monad+import Control.Monad.State hiding (State)+-- import System.IO (hPutStrLn, stderr) -- just for debugging++import Perception+import Display2 hiding (display)+import Message+import State++newtype Action a = Action+ { runAction ::+ forall r .+ Session ->+ IO r -> -- shutdown cont+ Perception -> -- cached perception+ (State -> Message -> a -> IO r) -> -- continuation+ IO r -> -- failure/reset cont+ State -> -- current state+ Message -> -- current message+ IO r+ }++instance Monad Action where+ return = returnAction+ (>>=) = bindAction++-- | Invokes the continuation.+returnAction :: a -> Action a+returnAction x = Action (\ s e p k a st m -> k st m x)++-- | Distributes the session and shutdown continuation,+-- threads the state and message.+bindAction :: Action a -> (a -> Action b) -> Action b+bindAction m f = Action (\ s e p k a st ms ->+ let next nst nm x =+ runAction (f x) s e p k a nst nm+ in runAction m s e p next a st ms)++instance MonadIO Action where+ liftIO x = Action (\ s e p k a st ms -> x >>= k st ms)++instance MonadState State Action where+ get = Action (\ s e p k a st ms -> k st ms st)+ put nst = Action (\ s e p k a st ms -> k nst ms ())++-- | Exported function to run the monad.+handlerToIO :: Session -> State -> Message -> Action () -> IO ()+handlerToIO session state msg h =+ runAction h+ session+ (shutdown session) -- get out of the game+ (perception_ state) -- cached perception+ (\ _ _ x -> return x) -- final continuation returns result+ (ioError $ userError "unhandled abort")+ state+ msg++-- | Invoking a session command.+session :: (Session -> Action a) -> Action a+session f = Action (\ s e p k a st ms -> runAction (f s) s e p k a st ms)++-- | Invoking a session command.+sessionIO :: (Session -> IO a) -> Action a+sessionIO f = Action (\ s e p k a st ms -> f s >>= k st ms)++-- | Display the current level, with the current message.+display :: Action Bool+display = Action (\ s e p k a st ms -> displayLevel s p st ms Nothing >>= k st ms)++-- | Display an overlay on top of the current screen.+overlay :: String -> Action Bool+overlay txt = Action (\ s e p k a st ms -> displayLevel s p st ms (Just txt) >>= k st ms)++-- | Set the current message.+message :: Message -> Action ()+message nm = Action (\ s e p k a st ms -> k st nm ())++-- | Add to the current message.+messageAdd :: Message -> Action ()+messageAdd nm = Action (\ s e p k a st ms -> k st (addMsg ms nm) ())++-- | Clear the current message.+resetMessage :: Action Message+resetMessage = Action (\ s e p k a st ms -> k st "" ms)++-- | Get the current message.+currentMessage :: Action Message+currentMessage = Action (\ s e p k a st ms -> k st ms ms)++-- | End the game, i.e., invoke the shutdown continuation.+end :: Action ()+end = Action (\ s e p k a st ms -> e)++-- | Reset the state and resume from the last backup point, i.e., invoke+-- the failure continuation.+abort :: Action a+abort = Action (\ s e p k a st ms -> a)++-- | Set the current exception handler. First argument is the handler,+-- second is the computation the handler scopes over.+tryWith :: Action () -> Action () -> Action ()+tryWith exc h = Action (\ s e p k a st ms -> runAction h s e p k (runAction exc s e p k a st ms) st ms)++-- | Takes a handler and a computation. If the computation fails, the+-- handler is invoked and then the computation is retried.+tryRepeatedlyWith :: Action () -> Action () -> Action ()+tryRepeatedlyWith exc h = tryWith (exc >> tryRepeatedlyWith exc h) h++-- | Try the given computation and silently catch failure.+try :: Action () -> Action ()+try = tryWith (return ())++-- | Try the given computation until it succeeds without failure.+tryRepeatedly :: Action () -> Action ()+tryRepeatedly = tryRepeatedlyWith (return ())++-- | Print a debug message or ignore.+debug :: String -> Action ()+debug x = return () -- liftIO $ hPutStrLn stderr x++-- | Print the given message, then abort.+abortWith :: Message -> Action a+abortWith msg =+ do+ message msg+ display+ abort++-- | Abort, and print the given message if the condition is true.+abortIfWith :: Bool -> Message -> Action a+abortIfWith True = abortWith+abortIfWith False = const abort++-- | Print message, await confirmation. Return value indicates if the+-- player tried to abort/escape.+messageMoreConfirm :: Message -> Action Bool+messageMoreConfirm msg =+ do+ message (msg ++ more)+ display+ session getConfirm++-- | Print a yes/no question and return the player's answer.+messageYesNo :: Message -> Action Bool+messageYesNo msg =+ do+ message (msg ++ yesno)+ display+ session getYesNo++-- | Print a message and an overlay, await confirmation. Return value+-- indicates if the player tried to abort/escape.+messageOverlayConfirm :: Message -> String -> Action Bool+messageOverlayConfirm msg txt = messageOverlaysConfirm msg [txt]++-- | Prints several overlays, one per page, and awaits confirmation.+-- Return value indicates if the player tried to abort/escape.+messageOverlaysConfirm :: Message -> [String] -> Action Bool+messageOverlaysConfirm msg [] =+ do+ resetMessage+ display+ return True+messageOverlaysConfirm msg (x:xs) =+ do+ message msg+ b <- overlay (x ++ more)+ if b+ then do+ b <- session getConfirm+ if b+ then do+ messageOverlaysConfirm msg xs+ else stop+ else stop+ where+ stop =+ do+ resetMessage+ display+ return False++-- | Update the cached perception for the given computation.+withPerception :: Action () -> Action ()+withPerception h = Action (\ s e _ k a st ms ->+ runAction h s e (perception_ st) k a st ms)++-- | Get the current perception.+currentPerception :: Action Perception+currentPerception = Action (\ s e p k a st ms -> k st ms p)+
+ src/Actions.hs view
@@ -0,0 +1,593 @@+module Actions where++import Control.Monad+import Control.Monad.State hiding (State)+import Data.Function+import Data.List as L+import Data.Map as M+import Data.Maybe+import Data.Set as S+import System.Time++import Action+import Actor hiding (updateActor)+import Display2 hiding (display)+import Dungeon+import Geometry+import Grammar+import qualified HighScores as H+import Item+import qualified Keys as K+import Level+import LevelState+import Message+import Monster+import Perception+import Random+import qualified Save as S+import State++displayHistory :: Action ()+displayHistory =+ do+ hst <- gets shistory+ messageOverlayConfirm "" (unlines hst)+ abort++saveGame :: Action ()+saveGame =+ do+ b <- messageYesNo "Really save?"+ if b+ then do+ -- Save the game state+ st <- get+ liftIO $ S.saveGame st+ let total = calculateTotal (splayer st)+ handleScores False False False total+ end+ else abortWith "Game resumed."++quitGame :: Action ()+quitGame =+ do+ b <- messageYesNo "Really quit?"+ if b+ then end -- TODO: why no highscore?+ else abortWith "Game resumed."++move :: Dir -> Action ()+move = moveOrAttack True True APlayer++run :: Dir -> Action ()+run dir =+ do+ modify (updatePlayer (\ p -> p { mdir = Just dir }))+ moveOrAttack False False APlayer dir -- attacks and opening doors disallowed while running++-- | This function implements the actual "logic" of running. It checks if we+-- have to stop running because something interested happened, and it checks+-- if we have to adjust the direction because we're in the corner of a corridor.+continueRun :: Dir -> Action ()+continueRun dir =+ do+ state <- get+ let lvl @(Level { lmonsters = ms, lmap = lmap }) = slevel state+ let player@(Monster { mloc = loc }) = splayer state+ let mslocs = S.fromList (L.map mloc ms)+ let t = lmap `at` loc -- tile at current location+ per <- currentPerception+ msg <- currentMessage+ let monstersVisible = not (S.null (mslocs `S.intersection` pvisible per))+ let newsReported = not (L.null msg)+ let itemsHere = not (L.null (titems t))+ let dirOK = accessible lmap loc (loc `shift` dir)+ -- What happens next is mostly depending on the terrain we're currently on.+ let exit (Stairs {}) = True+ exit (Opening {}) = True+ exit (Door {}) = True+ exit _ = False+ let hop t+ | monstersVisible || newsReported || itemsHere || exit t = abort+ hop Corridor =+ -- in corridors, explore all corners and stop at all crossings+ let ns = L.filter (\ x -> distance (neg dir, x) > 1+ && accessible lmap loc (loc `shift` x))+ moves+ allCloseTo main = L.all (\ d -> distance (main, d) <= 1) ns+ in case ns of+ [onlyDir] -> run onlyDir -- can be diagonal+ _ ->+ -- prefer orthogonal to diagonal dirs, for hero's safety+ case L.filter (\ x -> not $ diagonal x) ns of+ [ortoDir]+ | allCloseTo ortoDir -> run ortoDir+ _ -> abort+ hop _ -- outside corridors, never change direction+ | not dirOK = abort+ hop _ =+ let ns = L.filter (\ x -> x /= dir && distance (neg dir, x) > 1) moves+ ls = L.map (loc `shift`) ns+ as = L.filter (\ x -> accessible lmap loc x+ || openable 0 lmap x) ls+ ts = L.map (tterrain . (lmap `at`)) as+ in if L.any exit ts then abort else run dir+ hop (tterrain t)++stopRunning :: Action ()+stopRunning = modify (updatePlayer (\ p -> p { mdir = Nothing }))++ifRunning :: (Dir -> Action a) -> Action a -> Action a+ifRunning t e =+ do+ mdir <- gets (mdir . splayer)+ maybe e t mdir++-- | Store current message in the history and reset current message.+history :: Action ()+history =+ do+ msg <- resetMessage+ unless (L.null msg) $+ modify (updateHistory (take 500 . ((msg ++ " "):)))+ -- TODO: make history max configurable++-- | Update player memory.+remember :: Action ()+remember =+ do+ per <- currentPerception+ let vis = S.toList (pvisible per)+ let rememberLoc = M.update (\ (t,_) -> Just (t,t))+ modify (updateLevel (updateLMap (\ lmap -> foldr rememberLoc lmap vis)))++checkHeroDeath :: Action ()+checkHeroDeath =+ do+ player <- gets splayer+ let php = mhp player+ when (php <= 0) $ do+ messageAdd more+ display+ session getConfirm+ go <- messageMoreConfirm "You die."+ when go $ do+ let total = calculateTotal player+ handleScores True True False total+ end++neverMind :: Bool -> Action a+neverMind b = abortIfWith b "never mind"++-- | Open and close doors+openclose :: Bool -> Action ()+openclose o =+ do+ message "direction?"+ display+ e <- session nextCommand+ handleDirection e (actorOpenClose APlayer True o) (neverMind True)++actorOpenClose :: Actor ->+ Bool -> -- ^ verbose?+ Bool -> -- ^ open?+ Dir -> Action ()+actorOpenClose actor v o dir =+ do+ let txt = if o then "open" else "closed"+ state <- get+ let lvl@Level { lmonsters = ms, lmap = lmap } = slevel state+ let loc = mloc (getActor state actor)+ let isPlayer = actor == APlayer+ let isVerbose = v && isPlayer+ let dloc = shift loc dir -- location we act upon+ in case lmap `at` dloc of+ Tile d@(Door hv o') []+ | secret o' && isPlayer-> -- door is secret, cannot be opened or closed by hero+ neverMind isVerbose+ | toOpen (not o) /= o' -> -- door is in unsuitable state+ abortIfWith isVerbose ("already " ++ txt)+ | not (unoccupied ms lmap dloc) ->+ -- door is blocked by a monster+ abortIfWith isVerbose "blocked"+ | otherwise -> -- door can be opened / closed+ -- TODO: print message if action performed by monster and perceived+ let nt = Tile (Door hv (toOpen o)) []+ clmap = M.adjust (\ (_, mt) -> (nt, mt)) dloc lmap+ in modify (updateLevel (const (updateLMap (const clmap) lvl)))+ Tile d@(Door hv o') _ -> -- door is jammed by items+ abortIfWith isVerbose "jammed"+ _ -> -- there is no door here+ neverMind isVerbose++-- | Perform a level change -- will quit the game if the player leaves+-- the dungeon.+lvlchange :: VDir -> Action ()+lvlchange vdir =+ do+ state <- get+ let lvl @(Level { lmap = lmap }) = slevel state+ let player@(Monster { mloc = ploc }) = splayer state+ case lmap `at` ploc of+ Tile (Stairs _ vdir' next) is+ | vdir == vdir' -> -- stairs are in the right direction+ case next of+ Nothing ->+ -- we are at the "end" of the dungeon+ fleeDungeon+ Just (nln, nloc) ->+ -- perform level change+ do+ -- put back current level+ -- (first put back, then get, in case we change to the same level!)+ let full = putDungeonLevel lvl (sdungeon state)+ -- get new level+ let (new, ndng) = getDungeonLevel nln full+ modify (\ s -> s { sdungeon = ndng, slevel = new })+ modify (updatePlayer (\ p -> p { mloc = nloc }))+ _ -> -- no stairs+ do+ let txt = if vdir == Up then "up" else "down"+ abortWith ("no stairs " ++ txt)++-- | Hero has left the dungeon.+fleeDungeon :: Action ()+fleeDungeon =+ do+ player@(Monster { mitems = items }) <- gets splayer+ let total = calculateTotal player+ if total == 0+ then do+ messageMoreConfirm "Coward!"+ messageMoreConfirm "Next time try to grab some loot before you flee!"+ end+ else do+ let winMsg = "Congratulations, you won! Your loot, worth " +++ show total ++ " gold, is:"+ displayItems winMsg True items+ go <- session getConfirm+ when go $ handleScores True False True total+ end++-- | Calculate loot's worth. TODO: move to another module, and refine significantly.+calculateTotal :: Player -> Int+calculateTotal player = L.sum $ L.map price $ mitems player+ where+ price i = if iletter i == Just '$' then icount i else 10 * icount i++-- | Handle current score and display it with the high scores. TODO: simplify. Scores+-- should not be shown during the game, because ultimately the worth of items might give+-- information about the nature of the items.+handleScores :: Bool -> Bool -> Bool -> Int -> Action ()+handleScores write killed victor total =+ unless (total == 0) $ do+ nm <- gets (lname . slevel)+ cfg <- gets config+ time <- gets stime+ let points = if killed then (total + 1) `div` 2 else total+ let current = levelNumber nm -- TODO: rather use name of level+ curDate <- liftIO getClockTime+ let score = H.ScoreRecord+ points (-time) curDate current killed victor+ (placeMsg, slideshow) <- liftIO $ H.register cfg write score+ messageOverlaysConfirm placeMsg slideshow+ return ()++-- | Search for secret doors+search :: Action ()+search =+ do+ Level { lmap = lmap } <- gets slevel+ Monster { mloc = ploc } <- gets splayer+ let searchTile (Tile (Door hv (Just n)) x,t') = Just (Tile (Door hv (Just (max (n - 1) 0))) x, t')+ searchTile t = Just t+ slmap = foldl (\ l m -> update searchTile (shift ploc m) l) lmap moves+ modify (updateLevel (updateLMap (const slmap)))++-- | Look around at current location+lookAround :: Action a+lookAround =+ do+ state <- get+ let lvl@(Level { lmap = lmap }) = slevel state+ let Monster { mloc = ploc } = splayer state+ -- general info about current loc+ let lookMsg = lookAt True state lmap ploc+ -- check if there's something lying around at current loc+ let t = lmap `at` ploc+ if length (titems t) <= 2+ then do+ abortWith lookMsg+ else do+ displayItems lookMsg False (titems t)+ session getConfirm+ abortWith ""++-- | Display inventory+inventory :: Action a+inventory =+ do+ player <- gets splayer+ if L.null (mitems player)+ then abortWith "You are not carrying anything"+ else do+ displayItems "This is what you are carrying:" True (mitems player)+ session getConfirm+ abortWith ""++-- | Given item is now known to the player.+discover :: Item -> Action ()+discover i = modify (updateDiscoveries (S.insert (itype i)))++drinkPotion :: Action ()+drinkPotion =+ do+ state <- get+ let lvl @(Level { lmap = lmap }) = slevel state+ let player@(Monster { mloc = ploc }) = splayer state+ if L.null (mitems player)+ then abortWith "You are not carrying anything."+ else do+ i <- getPotion "What to drink?" (mitems player) "inventory"+ case i of+ Just i'@(Item { itype = Potion ptype }) ->+ do+ -- only one potion is consumed even if several are joined in the inventory+ let consumed = i' { icount = 1 }+ removeFromInventory consumed+ message (subjectVerbIObject state player "drink" consumed "")+ -- the potion is identified after drinking+ discover i'+ case ptype of+ PotionWater -> messageAdd "Tastes like water."+ PotionHealing -> do+ messageAdd "You feel better."+ modify (updatePlayer (\ p -> p { mhp = min (mhpmax p) (mhp p + playerHP `div` 4) }))+ Just _ -> abortWith "you cannot drink that"+ Nothing -> neverMind True++dropItem :: Action ()+dropItem =+ do+ state <- get+ let player@(Monster { mloc = ploc }) = splayer state+ if L.null (mitems player)+ then abortWith "You are not carrying anything."+ else do+ i <- getAnyItem "What to drop?" (mitems player) "inventory"+ case i of+ Just i' ->+ do+ removeFromInventory i'+ message (subjectVerbIObject state player "drop" i' "")+ dropItemsAt [i'] ploc+ Nothing -> neverMind True++dropItemsAt :: [Item] -> Loc -> Action ()+dropItemsAt is loc = modify (updateLevel (scatterItems is loc))++-- | Remove given item from the hero's inventory.+removeFromInventory :: Item -> Action ()+removeFromInventory i =+ modify (updatePlayer (\ p -> p { mitems = removeItemByLetter i (mitems p) }))++-- | Remove given item from the given location.+removeFromLoc :: Item -> Loc -> Action ()+removeFromLoc i loc =+ modify (updateLevel (\ l -> l { lmap = M.adjust (\ (t, rt) -> (update t, rt)) loc (lmap l) }))+ where+ update t = t { titems = removeItemByType i (titems t) }++-- | Let the player choose any potion. Note that this does not guarantee a potion to be chosen,+-- as the player can override the choice.+getPotion :: String -> -- prompt+ [Item] -> -- all objects in question+ String -> -- how to refer to the collection of objects, e.g. "in your inventory"+ Action (Maybe Item)+getPotion prompt is isn = getItem prompt (\ i -> case itype i of Potion {} -> True; _ -> False)+ "Potions" is isn++actorPickupItem :: Actor -> Action ()+actorPickupItem actor =+ do+ state <- get+ per <- currentPerception+ let lvl@(Level { lmap = lmap }) = slevel state+ let monster = getActor state actor+ let loc = mloc monster+ let t = lmap `at` loc -- the map tile in question+ let perceived = loc `S.member` pvisible per+ let isPlayer = actor == APlayer+ -- check if something is here to pick up+ case titems t of+ [] -> abortIfWith isPlayer "nothing here"+ (i:rs) -> -- pick up first item; TODO: let player select item; not for monsters+ case assignLetter (iletter i) (mletter monster) (mitems monster) of+ Just l ->+ do+ let (ni, nitems) = joinItem (i { iletter = Just l }) (mitems monster)+ -- message is dependent on who picks up and if the hero can perceive it+ if isPlayer+ then message (letterLabel (iletter ni) ++ objectItem state (icount ni) (itype ni))+ else when perceived $+ message $ subjectCompoundVerbIObject state monster "pick" "up" i ""+ removeFromLoc i loc+ -- add item to actor's inventory:+ updateActor actor $ \ m ->+ m { mitems = nitems, mletter = maxLetter l (mletter monster) }+ Nothing -> abortIfWith isPlayer "you cannot carry any more"++-- | Replaces the version in Actor module+updateActor :: Actor -> -- ^ who to update+ (Monster -> Monster) -> -- ^ the update+ Action ()+updateActor (AMonster n) f =+ do+ monsters <- gets (lmonsters . slevel)+ let (m, ms) = updateMonster f n monsters+ modify (updateLevel (updateMonsters (const ms)))+updateActor APlayer f =+ modify (updatePlayer f)++pickupItem :: Action ()+pickupItem = actorPickupItem APlayer++-- TODO: I think that player handlers should be wrappers around more general actor handlers, but+-- the actor handlers should be performing specific actions, i.e., already specify the item to be+-- picked up. It doesn't make sense to invoke dialogues for arbitrary actors, and most likely the+-- decision for a monster is based on perceiving a particular item to be present, so it's already+-- known. In actor handlers we should make sure that messages are printed to the player only if the+-- hero can perceive the action.++-- | Let the player choose any item from a list of items.+getAnyItem :: String -> -- prompt+ [Item] -> -- all objects in question+ String -> -- how to refer to the collection of objects, e.g. "in your inventory"+ Action (Maybe Item)+getAnyItem prompt is isn = getItem prompt (const True) "Objects" is isn++-- | Let the player choose a single item from a list of items.+getItem :: String -> -- prompt message+ (Item -> Bool) -> -- which items to consider suitable+ String -> -- how to describe suitable objects+ [Item] -> -- all objects in question+ String -> -- how to refer to the collection of objects, e.g. "in your inventory"+ Action (Maybe Item)+getItem prompt p ptext is0 isn =+ let is = L.filter p is0+ choice | L.null is = "[*]"+ | otherwise = "[" ++ letterRange (concatMap (maybeToList . iletter) is) ++ " or ?*]"+ r = do+ message (prompt ++ " " ++ choice)+ display+ let h = session nextCommand >>= h'+ h' e = case e of+ K.Char '?' -> do+ -- filter for supposedly suitable objects+ b <- displayItems (ptext ++ " " ++ isn) True is+ if b then session (getOptionalConfirm (const r) h')+ else r+ K.Char '*' -> do+ -- show all objects+ b <- displayItems ("Objects " ++ isn) True is0+ if b then session (getOptionalConfirm (const r) h')+ else r+ K.Char l -> return (find (\ i -> maybe False (== l) (iletter i)) is0)+ _ -> return Nothing+ h+ in r++displayItems :: Message -> Bool -> [Item] -> Action Bool+displayItems msg sorted is =+ do+ state <- get+ let inv = unlines $+ L.map (\ (Item { icount = c, iletter = l, itype = t }) ->+ letterLabel l ++ objectItem state c t ++ " ")+ ((if sorted then sortBy (cmpLetter' `on` iletter) else id) is)+ let ovl = inv ++ more+ message msg+ overlay ovl++-- | This function performs a move (or attack) by any actor, i.e., it can handle+-- both monsters and the player.+moveOrAttack :: Bool -> -- allow attacks?+ Bool -> -- auto-open doors on move+ Actor -> -- who's moving?+ Dir ->+ Action ()+moveOrAttack allowAttacks autoOpen actor dir+ | dir == (0,0) =+ -- Moving with no direction is a noop. We include it currently to prevent that+ -- monsters attack themselves by accident.+ return ()+ | otherwise =+ do+ -- We start by looking at the target position.+ state <- get+ let lvl@(Level { lmap = lmap }) = slevel state+ let player = splayer state+ let monster = getActor state actor+ let loc = mloc monster -- current location+ let s = lmap `at` loc -- tile at current location+ let nloc = loc `shift` dir -- target location+ let t = lmap `at` nloc -- tile at target location+ let attackedPlayer = [ APlayer | mloc player == nloc ]+ let attackedMonsters = L.map AMonster $+ findIndices (\ m -> mloc m == nloc) (lmonsters lvl)+ let attacked :: [Actor]+ attacked = attackedPlayer ++ attackedMonsters+ -- At the moment, we check whether there is a monster before checking accessibility+ -- i.e., we can attack a monster on a blocked location. For instance,+ -- a monster on an open door can be attacked diagonally, and a+ -- monster capable of moving through walls can be attacked from an+ -- adjacent position.+ if not (L.null attacked)+ then if not allowAttacks then abort else do+ -- perform the attack+ mapM_ (actorAttackActor actor) attacked+ else if accessible lmap loc nloc then do+ -- perform the move+ updateActor actor (\ m -> m { mloc = nloc })+ when (actor == APlayer) $ message $ lookAt False state lmap nloc+ -- TODO: seems somewhat dubious to do this here, but perhaps it's ok+ else if autoOpen then+ -- try to check if there's a door we can open+ actorOpenClose actor False True dir+ else abort -- nothing useful we can do++actorAttackActor :: Actor -> Actor -> Action ()+actorAttackActor source target =+ do+ debug "actorAttackActor"+ state <- get+ let sm = getActor state source+ let tm = getActor state target+ -- determine the weapon used for the attack+ let sword = strongestSword (mitems sm)+ -- damage the target+ let newHp = mhp tm - 3 - sword+ let killed = newHp <= 0+ updateActor target $ \ m ->+ if killed+ then m { mhp = 0, mtime = 0 } -- grant an immediate move to die+ -- TODO: is there a good reason not to let the monster die just here?+ else m { mhp = newHp }+ -- determine how the hero perceives the event; TODO: we have to be more+ -- precise and treat cases where two monsters fight, but only one is visible+ let combatVerb = if killed && target /= APlayer then "kill" else "hit"+ let swordMsg = if sword == 0 then "" else+ " with a (+" ++ show sword ++ ") sword" -- TODO: generate proper message+ let combatMsg = subjectVerbMObject state sm combatVerb tm swordMsg+ per <- currentPerception+ let perceived = mloc sm `S.member` pvisible per+ messageAdd $+ if perceived+ then combatMsg+ else "You hear some noises."++-- | Generate a monster, possibly.+generateMonster :: Action ()+generateMonster =+ do+ lvl <- gets slevel+ player <- gets splayer+ nlvl <- liftIO $ rndToIO $ addMonster lvl player+ modify (updateLevel (const nlvl))++-- | Advance the move time for the given actor.+advanceTime :: Actor -> Action ()+advanceTime actor =+ do+ time <- gets stime+ updateActor actor (\ m -> m { mtime = time + mspeed m })++-- | Possibly regenerate HP for the given actor.+regenerate :: Actor -> Action ()+regenerate actor =+ do+ time <- gets stime+ -- TODO: remove hardcoded time interval, regeneration should be an attribute of the monster+ when (time `mod` 1500 == 0) $+ updateActor actor (\ m -> m { mhp = min (mhpmax m) (mhp m + 1) })+
+ src/Actor.hs view
@@ -0,0 +1,35 @@+module Actor where++import Level+import Monster+import State++data Actor = AMonster Int -- offset in monster list+ | APlayer+ deriving (Show, Eq)++getActor :: State -> Actor -> Monster+getActor (State { slevel = lvl, splayer = p }) a =+ case a of+ AMonster n -> lmonsters lvl !! n+ APlayer -> p++updateActor :: (Monster -> Monster) -> -- the update+ (Monster -> State -> IO a) -> -- continuation+ Actor -> -- who to update+ State -> IO a -- transformed continuation+updateActor f k (AMonster n) state@(State { slevel = lvl, splayer = p }) =+ let (m,ms) = updateMonster f n (lmonsters lvl)+ in k m (updateLevel (updateMonsters (const ms)) state)+updateActor f k APlayer state@(State { slevel = lvl, splayer = p }) =+ k p (updatePlayer f state)++updateMonster :: (Monster -> Monster) -> Int -> [Monster] ->+ (Monster, [Monster])+updateMonster f n ms =+ case splitAt n ms of+ (pre, x : post) -> let m = f x+ mtimeChanged = mtime x /= mtime m+ in (m, if mtimeChanged then snd (insertMonster m (pre ++ post))+ else pre ++ [m] ++ post)+ xs -> error "updateMonster"
+ src/Command.hs view
@@ -0,0 +1,32 @@+module Command where++import Action+import Actions+import Geometry+import qualified Keys as K+import Level++data Described a = Described { chelp :: String, caction :: a }+ | Undescribed { caction :: a }++type Command = Described (Action ())+type DirCommand = Described (Dir -> Action ())++closeCommand = Described "close a door" (openclose False)+openCommand = Described "open a door" (openclose True)+pickupCommand = Described "pick up an object" pickupItem+dropCommand = Described "drop an object" dropItem+inventoryCommand = Described "display inventory" inventory+searchCommand = Described "search for secret doors" search+ascendCommand = Described "ascend a level" (lvlchange Up)+descendCommand = Described "descend a level" (lvlchange Down)+lookCommand = Described "look around" lookAround+drinkCommand = Described "quaff a potion" drinkPotion+waitCommand = Described "wait" (return () :: Action ())+saveCommand = Described "save and quit the game" saveGame+quitCommand = Described "quit without saving" quitGame+historyCommand = Described "display previous messages" displayHistory++moveDirCommand = Described "move in direction" move+runDirCommand = Described "run in direction" run+
+ src/Config.hs view
@@ -0,0 +1,71 @@+module Config where++import System.Directory+import System.FilePath+import Control.Monad.Error++import Data.ConfigFile+import Data.Either.Utils+import Data.Maybe+import qualified Data.Binary as B++newtype CP = CP ConfigParser++instance B.Binary CP where+ put (CP config) = B.put $ to_string config+ get = do+ string <- B.get+ let parsed = readstring emptyCP string+ return $ CP $ forceEither $ parsed++instance Show CP where+ show (CP config) = show $ to_string config++-- | Underscore in the name to avoid name clash.+empty_CP :: CP+empty_CP = CP emptyCP++-- | Path to the main configuration file.+file :: IO String+file =+ do+ appData <- getAppUserDataDirectory "LambdaHack"+ return $ combine appData "LambdaHack.config"++-- | The configuration read from the main configuration file.+-- If no such file, generate empty configuration.+config :: IO CP+config =+ do+ f <- file+ b <- doesFileExist f+ if not b+ then return $ empty_CP+ else do+ c <- readfile emptyCP f+ return $ CP (forceEither c)++-- | A simplified access to an option in a given section,+-- with simple error reporting (no error is caught and hidden).+-- If there is no config file or no such option, gives Nothing.+getOption :: Get_C a => CP -> SectionSpec -> OptionSpec ->+ Maybe a+getOption (CP config) s o =+ do+ if has_option config s o+ then let val = get config s o+ valForced = forceEither val+ in Just valForced+ else Nothing++-- | Looks up a file path in the config file, faling back to the default path.+-- The path from the config file is taken relative to the home directory+-- and the default is taken relative to the current directory. In any case,+-- the returned path is absolute.+getFile :: CP -> FilePath -> SectionSpec -> OptionSpec -> IO FilePath+getFile config dflt s o =+ do+ current <- getCurrentDirectory+ appData <- getAppUserDataDirectory "LambdaHack"+ let path = getOption config s o+ return $ maybe (combine current dflt) (combine appData) path
+ src/Display.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE CPP #-}++module Display (module D) where++-- wrapper for selected Display frontend++#ifdef CURSES+import Display.Curses as D+#elif GTK+import Display.Gtk as D+#else+import Display.Vty as D+#endif+
+ src/Display/Curses.hs view
@@ -0,0 +1,125 @@+module Display.Curses+ (displayId, startup, shutdown,+ display, nextEvent, setBG, setFG, setBold, Session,+ white, black, yellow, blue, magenta, red, green, attr, Display.Curses.Attr) where++import UI.HSCurses.Curses as C hiding (setBold)+import qualified UI.HSCurses.CursesHelper as C+import Data.List as L+import Data.Map as M+import Data.Char+import qualified Data.ByteString as BS++import Geometry+import Keys as K++displayId = "curses"++data Session =+ Session+ { win :: Window,+ styles :: Map (Maybe AttrColor, Maybe AttrColor) C.CursesStyle }++startup :: (Session -> IO ()) -> IO ()+startup k =+ do+ C.start+ C.startColor+ cursSet CursorInvisible+ nr <- colorPairs+ let s = [ ((f,b), C.Style (toFColor f) (toBColor b))+ | f <- Nothing : L.map Just [minBound..maxBound],+ b <- Nothing : L.map Just [minBound..maxBound] ]+ let (ks, vs) = unzip (tail s) -- drop the Nothing/Nothing combo+ ws <- C.convertStyles (take (nr - 1) vs)+ k (Session C.stdScr (M.fromList (zip ks ws)))++shutdown :: Session -> IO ()+shutdown w = C.end++display :: Area -> Session -> (Loc -> (Display.Curses.Attr, Char)) -> String -> String -> IO ()+display ((y0,x0),(y1,x1)) (Session { win = w, styles = s }) f msg status =+ do+ erase+ mvWAddStr w 0 0 msg+ sequence_ [ let (a,c) = f (y,x) in C.setStyle (findWithDefault C.defaultCursesStyle a s) >> mvWAddStr w (y+1) x [c]+ | x <- [x0..x1], y <- [y0..y1] ]+ mvWAddStr w (y1+2) 0 status+ refresh+{-+ in V.update vty (Pic NoCursor + ((renderBS attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1-x0+1) msg)))) <->+ img <-> + (renderBS attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1-x0+1) status))))))+-}++{-+toWidth :: Int -> String -> String+toWidth n x = take n (x ++ repeat ' ')+-}++keyTranslate :: C.Key -> Maybe K.Key+keyTranslate e =+ case e of+ C.KeyChar '\ESC' -> Just K.Esc+ C.KeyExit -> Just K.Esc+ C.KeyChar '\n' -> Just K.Return+ C.KeyChar '\r' -> Just K.Return+ C.KeyEnter -> Just K.Return+ C.KeyUp -> Just K.Up+ C.KeyDown -> Just K.Down+ C.KeyLeft -> Just K.Left+ C.KeyRight -> Just K.Right+ C.KeyHome -> Just K.Home+ C.KeyPPage -> Just K.PgUp+ C.KeyEnd -> Just K.End+ C.KeyNPage -> Just K.PgDn+ C.KeyBeg -> Just K.Begin+ C.KeyB2 -> Just K.Begin+ C.KeyChar c -> Just (K.Char c)+ _ -> Nothing++nextEvent :: Session -> IO K.Key+nextEvent session =+ do+ e <- C.getKey refresh+ maybe (nextEvent session) return (keyTranslate e)++type Attr = (Maybe AttrColor, Maybe AttrColor)++attr = (Nothing, Nothing)++data AttrColor = White | Black | Yellow | Blue | Magenta | Red | Green + deriving (Show, Eq, Ord, Enum, Bounded)++toFColor :: Maybe AttrColor -> C.ForegroundColor+toFColor (Just White) = C.WhiteF+toFColor (Just Black) = C.BlackF+toFColor (Just Yellow) = C.BrownF+toFColor (Just Blue) = C.DarkBlueF+toFColor (Just Magenta) = C.PurpleF+toFColor (Just Red) = C.DarkRedF+toFColor (Just Green) = C.DarkGreenF+toFColor Nothing = C.DefaultF++toBColor :: Maybe AttrColor -> C.BackgroundColor+toBColor (Just White) = C.WhiteB+toBColor (Just Black) = C.BlackB+toBColor (Just Yellow) = C.BrownB+toBColor (Just Blue) = C.DarkBlueB+toBColor (Just Magenta) = C.PurpleB+toBColor (Just Red) = C.DarkRedB+toBColor (Just Green) = C.DarkGreenB+toBColor Nothing = C.DefaultB++white = White+black = Black+yellow = Yellow+blue = Blue+magenta = Magenta+red = Red+green = Green++setBold (f, b) = (f, b)+setFG c (_, b) = (Just c, b)+setBG c (f, _) = (f, Just c)
+ src/Display/Gtk.hs view
@@ -0,0 +1,214 @@+module Display.Gtk+ (displayId, startup, shutdown, + display, nextEvent, setBG, setFG, setBold, Session,+ white, black, yellow, blue, magenta, red, green, attr, Attr) where++import Control.Monad+import Control.Concurrent+import Graphics.UI.Gtk.Gdk.Events -- TODO: replace, deprecated+import Graphics.UI.Gtk hiding (Attr)+import Data.List as L+import Data.IORef+import Data.Map as M++import Geometry+import Keys as K++displayId = "gtk"++data Session =+ Session {+ schan :: Chan String,+ stags :: Map AttrKey TextTag,+ sview :: TextView }++doAttr :: TextTag -> AttrKey -> IO ()+doAttr tt (BG Blue) = set tt [ textTagBackground := "#0000CC" ]+doAttr tt (BG Magenta) = set tt [ textTagBackground := "#CC00CC" ]+doAttr tt (BG Green) = set tt [ textTagBackground := "#00CC00" ]+doAttr tt (BG Red) = set tt [ textTagBackground := "#CC0000" ]+doAttr tt (BG White) = set tt [ textTagBackground := "#FFFFFF" ]+doAttr tt (FG Green) = set tt [ textTagForeground := "#00FF00" ]+doAttr tt (FG Red) = set tt [ textTagForeground := "#FF0000" ]+doAttr tt (FG Blue) = set tt [ textTagForeground := "#0000FF" ]+doAttr tt (FG Yellow) = set tt [ textTagForeground := "#CCCC00" ]+doAttr tt (FG Black) = set tt [ textTagForeground := "#000000" ]+doAttr tt _ = return ()++startup :: (Session -> IO ()) -> IO ()+startup k =+ do+ -- initGUI+ unsafeInitGUIForThreadedRTS+ w <- windowNew++ ttt <- textTagTableNew+ -- text attributes+ tts <- fmap M.fromList $+ mapM (\ c -> do+ tt <- textTagNew Nothing+ textTagTableAdd ttt tt+ doAttr tt c+ return (c,tt))+ [ x | c <- [minBound .. maxBound], x <- [FG c, BG c]]++ -- text buffer+ tb <- textBufferNew (Just ttt)+ textBufferSetText tb (unlines (replicate 25 (replicate 80 ' ')))++ -- create text view+ tv <- textViewNewWithBuffer tb+ containerAdd w tv+ textViewSetEditable tv False+ textViewSetCursorVisible tv False++ -- font+ f <- fontDescriptionNew+ fontDescriptionSetFamily f "Monospace"+ fontDescriptionSetSize f 12+ widgetModifyFont tv (Just f)+ currentfont <- newIORef f+ onButtonPress tv (\ e -> case e of+ Button { Graphics.UI.Gtk.Gdk.Events.eventButton = RightButton } ->+ do+ fsd <- fontSelectionDialogNew "Choose font"+ cf <- readIORef currentfont+ fd <- fontDescriptionToString cf+ fontSelectionDialogSetFontName fsd fd+ fontSelectionDialogSetPreviewText fsd "+##@##-...|"+ response <- dialogRun fsd+ when (response == ResponseOk) $+ do+ fn <- fontSelectionDialogGetFontName fsd+ case fn of+ Just fn' -> do+ fd <- fontDescriptionFromString fn'+ writeIORef currentfont fd+ widgetModifyFont tv (Just fd)+ Nothing -> return ()+ widgetDestroy fsd+ return True+ _ -> return False)++ let black = Color minBound minBound minBound+ let white = Color maxBound maxBound maxBound+ widgetModifyBase tv StateNormal black+ widgetModifyText tv StateNormal white++ ec <- newChan + forkIO $ k (Session ec tts tv)+ + onKeyPress tv (\ e -> postGUIAsync (writeChan ec (Graphics.UI.Gtk.Gdk.Events.eventKeyName e)) >> return True)++ onDestroy w mainQuit -- set quit handler+ widgetShowAll w+ yield+ mainGUI++shutdown _ = mainQuit++display :: Area -> Session -> (Loc -> (Attr, Char)) -> String -> String -> IO ()+display ((y0,x0),(y1,x1)) session f msg status =+ postGUIAsync $+ do+ tb <- textViewGetBuffer (sview session)+ let text = unlines [ [ snd (f (y,x)) | x <- [x0..x1] ] | y <- [y0..y1] ]+ textBufferSetText tb (msg ++ "\n" ++ text ++ status)+ sequence_ [ setTo tb (stags session) (y,x) a | + y <- [y0..y1], x <- [x0..x1], let loc = (y,x), let (a,c) = f (y,x) ]++setTo :: TextBuffer -> Map AttrKey TextTag -> Loc -> Attr -> IO ()+setTo tb tts (ly,lx) a =+ do+ ib <- textBufferGetIterAtLineOffset tb (ly+1) lx+ ie <- textIterCopy ib+ textIterForwardChar ie+ mapM_ (\ c -> textBufferApplyTag tb (tts ! c) ib ie) a++-- | reads until a non-dead key encountered+readUndeadChan :: Chan String -> IO String+readUndeadChan ch =+ do+ x <- readChan ch+ if dead x then readUndeadChan ch else return x+ where+ dead x =+ case x of+ "Shift_R" -> True+ "Shift_L" -> True+ "Control_L" -> True+ "Control_R" -> True+ "Super_L" -> True+ "Super_R" -> True+ "Menu" -> True+ "Alt_L" -> True+ "Alt_R" -> True+ "ISO_Level2_Shift" -> True+ "ISO_Level3_Shift" -> True+ "ISO_Level2_Latch" -> True+ "ISO_Level3_Latch" -> True+ "Num_Lock" -> True+ "Caps_Lock" -> True+ _ -> False++keyTranslate :: String -> Maybe K.Key+keyTranslate "less" = Just (K.Char '<')+keyTranslate "greater" = Just (K.Char '>')+keyTranslate "period" = Just (K.Char '.')+keyTranslate "colon" = Just (K.Char ':')+keyTranslate "comma" = Just (K.Char ',')+keyTranslate "space" = Just (K.Char ' ')+keyTranslate "question" = Just (K.Char '?')+keyTranslate "asterisk" = Just (K.Char '*')+keyTranslate "Escape" = Just K.Esc+keyTranslate "Return" = Just K.Return+keyTranslate "KP_Up" = Just K.Up+keyTranslate "KP_Down" = Just K.Down+keyTranslate "KP_Left" = Just K.Left+keyTranslate "KP_Right" = Just K.Right+keyTranslate "KP_Home" = Just K.Home+keyTranslate "KP_End" = Just K.End+keyTranslate "KP_Page_Up" = Just K.PgUp+keyTranslate "KP_Page_Down" = Just K.PgDn+keyTranslate "KP_Begin" = Just K.Begin+keyTranslate "KP_Enter" = Just K.Return+keyTranslate ['K','P','_',c] = Just (K.Char c) -- for numbers+keyTranslate [c] = Just (K.Char c)+keyTranslate _ = Nothing++nextEvent :: Session -> IO K.Key+nextEvent session =+ do+ e <- readUndeadChan (schan session)+ maybe (nextEvent session) return (keyTranslate e)++setBold = id -- not supported yet+setBG c = (BG c :)+setFG c = (FG c :)+blue = Blue+magenta = Magenta+red = Red+yellow = Yellow+green = Green+white = White+black = Black+attr = []++type Attr = [AttrKey]++data AttrKey =+ FG AttrColor+ | BG AttrColor+ deriving (Eq, Ord)++type Color = AttrColor++data AttrColor =+ Blue+ | Magenta+ | Red+ | Green+ | Yellow+ | White+ | Black+ deriving (Eq, Ord, Enum, Bounded)
+ src/Display/Vty.hs view
@@ -0,0 +1,64 @@+module Display.Vty+ (displayId, startup, shutdown,+ display, nextEvent, setBold, setBG, setFG, Session,+ white, black, yellow, blue, magenta, red, green, attr, Attr) where++import Graphics.Vty as V+import Data.List as L+import Data.Char+import qualified Data.ByteString as BS++import Geometry+import Keys as K++displayId = "vty"++type Session = V.Vty++startup :: (Session -> IO ()) -> IO ()+startup k = V.mkVty >>= k++display :: Area -> Session -> (Loc -> (Attr, Char)) -> String -> String -> IO ()+display ((y0,x0),(y1,x1)) vty f msg status =+ let img = (foldr (<->) V.empty_image .+ L.map (foldr (<|>) V.empty_image .+ L.map (\ (x,y) -> let (a,c) = f (y,x) in char a c)))+ [ [ (x,y) | x <- [x0..x1] ] | y <- [y0..y1] ]+ in V.update vty (pic_for_image+ (utf8_bytestring attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1 - x0 + 1) msg))) <->+ img <-> + utf8_bytestring attr (BS.pack (L.map (fromIntegral . ord) (toWidth (x1 - x0 + 1) status)))))++toWidth :: Int -> String -> String+toWidth n x = take n (x ++ repeat ' ')++keyTranslate :: V.Event -> Maybe K.Key+keyTranslate e =+ case e of+ V.EvKey KEsc [] -> Just K.Esc+ V.EvKey KEnter [] -> Just K.Return+ V.EvKey KUp [] -> Just K.Up+ V.EvKey KDown [] -> Just K.Down+ V.EvKey KLeft [] -> Just K.Left+ V.EvKey KRight [] -> Just K.Right+ V.EvKey KHome [] -> Just K.Home+ V.EvKey KPageUp [] -> Just K.PgUp+ V.EvKey KEnd [] -> Just K.End+ V.EvKey KPageDown [] -> Just K.PgDn+ V.EvKey KBegin [] -> Just K.Begin+ V.EvKey (KASCII c) [] -> Just (K.Char c)+ _ -> Nothing++nextEvent :: Session -> IO K.Key+nextEvent session =+ do+ e <- V.next_event session+ maybe (nextEvent session) return (keyTranslate e)++attr = def_attr++setBold a = with_style a bold++setBG c a = with_back_color a c++setFG c a = with_fore_color a c
+ src/Display2.hs view
@@ -0,0 +1,194 @@+module Display2 (module Display, module Display2) where++-- Display routines that are independent of the selected display frontend.++import Data.Set as S+import Data.List as L+import Data.Map as M+import Control.Monad.State hiding (State) -- for MonadIO, seems to be portable between mtl-1 and 2++import Message+import Display+import State+import Geometry+import Level+import LevelState+import Dungeon+import Perception+import Monster+import Item+import Keys as K++-- | Next event translated to a canonical form+nextCommand :: MonadIO m => Session -> m Key+nextCommand session =+ do+ e <- liftIO $ nextEvent session+ return (canonicalKey e)++-- | maps a key to the canonical key for the command it denotes+canonicalKey :: Key -> Key+canonicalKey e =+ case e of+ K.Char '8' -> K.Char 'K'+ K.Char '2' -> K.Char 'J'+ K.Char '4' -> K.Char 'H'+ K.Char '6' -> K.Char 'L'+ K.Char '7' -> K.Char 'Y'+ K.Char '9' -> K.Char 'U'+ K.Char '1' -> K.Char 'B'+ K.Char '3' -> K.Char 'N'+ K.Char '5' -> K.Char '.'+ K.Up -> K.Char 'k'+ K.Down -> K.Char 'j'+ K.Left -> K.Char 'h'+ K.Right -> K.Char 'l'+ K.Home -> K.Char 'y'+ K.PgUp -> K.Char 'u'+ K.End -> K.Char 'b'+ K.PgDn -> K.Char 'n'+ K.Begin -> K.Char '.'+ k -> k++-- | Displays a message on a blank screen. Waits for confirmation.+displayBlankConfirm :: Session -> String -> IO Bool+displayBlankConfirm session txt =+ let x = txt ++ more+ in do+ display ((0, 0), normalLevelSize) session (const (attr, ' ')) x ""+ getConfirm session++-- | Waits for a space or return.+getConfirm :: MonadIO m => Session -> m Bool+getConfirm session =+ getOptionalConfirm return (const $ getConfirm session) session++getOptionalConfirm :: MonadIO m => (Bool -> m a) -> (Key -> m a) -> Session -> m a+getOptionalConfirm h k session =+ do+ e <- liftIO $ nextCommand session+ case e of+ K.Char ' ' -> h True+ K.Return -> h True+ K.Esc -> h False+ _ -> k e++-- | A yes-no confirmation.+getYesNo :: MonadIO m => Session -> m Bool+getYesNo session =+ do+ e <- liftIO $ nextCommand session+ case e of+ K.Char 'y' -> return True+ K.Char 'n' -> return False+ K.Esc -> return False+ _ -> getYesNo session++-- | Configurable event handler for the direction keys. Is used to+-- handle player moves, but can also be used for directed commands+-- such as open/close.+handleDirection :: Key -> (Dir -> a) -> a -> a+handleDirection e h k =+ case e of+ K.Char 'k' -> h up+ K.Char 'j' -> h down+ K.Char 'h' -> h left+ K.Char 'l' -> h right+ K.Char 'y' -> h upleft+ K.Char 'u' -> h upright+ K.Char 'b' -> h downleft+ K.Char 'n' -> h downright+ _ -> k++-- | Configurable event handler for the upper direction keys. Is used to+-- handle player moves, but can also be used for directed commands+-- such as open/close.+handleUDirection :: Key -> (Dir -> a) -> a -> a+handleUDirection e h k =+ case e of+ K.Char 'K' -> h up+ K.Char 'J' -> h down+ K.Char 'H' -> h left+ K.Char 'L' -> h right+ K.Char 'Y' -> h upleft+ K.Char 'U' -> h upright+ K.Char 'B' -> h downleft+ K.Char 'N' -> h downright+ _ -> k++splitOverlay :: Int -> String -> [[String]]+splitOverlay s xs = splitOverlay' (lines xs)+ where+ splitOverlay' ls+ | length ls <= s = [ls] -- everything fits on one screen+ | otherwise = let (pre,post) = splitAt (s - 1) ls+ in (pre ++ [more]) : splitOverlay' post++-- | Returns a function that looks up the characters in the+-- string by location. Takes the height of the display plus+-- the string. Returns also the number of screens required+-- to display all of the string.+stringByLocation :: Y -> String -> (Int, Loc -> Maybe Char)+stringByLocation sy xs =+ let+ ls = splitOverlay sy xs+ m = M.fromList (zip [0..] (L.map (M.fromList . zip [0..]) (concat ls)))+ k = length ls+ in+ (k, \ (y,x) -> M.lookup y m >>= \ n -> M.lookup x n)++displayLevel :: Session -> Perception -> State -> Message -> Maybe String -> IO Bool+displayLevel session per+ (state@(State { splayer = player@(Monster { mhpmax = phpmax, mhp = php, mdir = pdir, mloc = ploc }),+ stime = time,+ sassocs = assocs,+ slevel = lvl@(Level nm sz@(sy,sx) ms smap nlmap lmeta) }))+ msg moverlay =+ let+ overlay = maybe "" id moverlay+ reachable = preachable per+ visible = pvisible per+ sSml = ssensory state == Smell+ sVis = case ssensory state of Vision _ -> True; _ -> False+ sOmn = sdisplay state == Omniscient+ sTer = case sdisplay state of Terrain n -> n; _ -> 0+ lAt = if sOmn || sTer > 0 then at else rememberAt+ lVision = if sVis+ then \ vis rea ->+ if vis then setBG blue+ else if rea then setBG magenta+ else id+ else \ vis rea -> id+ (n,over) = stringByLocation (sy+1) overlay -- n is the number of overlay screens+ gold = maybe 0 (icount . fst) $ findItem (\ i -> iletter i == Just '$') (mitems player)+ disp n msg =+ display ((0,0),sz) session+ (\ loc -> let tile = nlmap `lAt` loc+ sml = ((smap ! loc) - time) `div` 100+ vis = S.member loc visible+ rea = S.member loc reachable+ (rv,ra) = case L.find (\ m -> loc == mloc m) (player:ms) of+ _ | sTer > 0 -> viewTerrain sTer False (tterrain tile)+ Just m | sOmn || vis -> viewMonster (mtype m)+ _ | sSml && sml >= 0 -> viewSmell sml+ | otherwise -> viewTile vis tile assocs+ vision = lVision vis rea+ in+ case over (loc `shift` ((sy+1) * n, 0)) of+ Just c -> (attr, c)+ _ -> (ra . vision $ attr, rv))+ msg+ (take 40 (levelName nm ++ repeat ' ') +++ take 10 ("$: " ++ show gold ++ repeat ' ') +++ take 15 ("HP: " ++ show php ++ " (" ++ show phpmax ++ ")" ++ repeat ' ') +++ take 10 ("T: " ++ show (time `div` 10) ++ repeat ' '))+ msgs = splitMsg sx msg+ perf k [] = perfo k ""+ perf k [xs] = perfo k xs+ perf k (x:xs) = disp n (x ++ more) >> getConfirm session >>= \ b ->+ if b then perf k xs else return False+ perfo k xs+ | k < n - 1 = disp k xs >> getConfirm session >>= \ b ->+ if b then perfo (k+1) xs else return False+ | otherwise = disp k xs >> return True+ in perf 0 msgs
+ src/Dungeon.hs view
@@ -0,0 +1,324 @@+module Dungeon where++import Prelude hiding (floor)+import Control.Monad++import Data.Map as M+import Data.List as L+import Data.Ratio++import State+import Geometry+import Level+import Monster+import Item+import Random++type Corridor = [(Y,X)]+type Room = Area++-- | Create a random room according to given parameters.+mkRoom :: Int -> -- ^ border columns+ (Y,X) -> -- ^ minimum size+ Area -> -- ^ this is an area, not the room itself+ Rnd Room -- ^ this is the upper-left and lower-right corner of the room+mkRoom bd (ym,xm) ((y0,x0),(y1,x1)) =+ do+ (ry0,rx0) <- locInArea ((y0 + bd,x0 + bd),(y1 - bd - ym + 1,x1 - bd - xm + 1))+ (ry1,rx1) <- locInArea ((ry0 + ym - 1,rx0 + xm - 1),(y1 - bd,x1 - bd))+ return ((ry0,rx0),(ry1,rx1))++-- | Create a no-room, i.e., a single corridor field.+mkNoRoom :: Int -> -- ^ border columns+ Area -> -- ^ this is an area, not the room itself+ Rnd Room -- ^ this is the upper-left and lower-right corner of the room+mkNoRoom bd ((y0,x0),(y1,x1)) =+ do+ (ry,rx) <- locInArea ((y0 + bd,x0 + bd),(y1 - bd,x1 - bd))+ return ((ry,rx),(ry,rx))++-- | Create a corridor, either horizontal or vertical, with+-- a possible intermediate part that is in the opposite direction.+mkCorridor :: HV -> (Loc,Loc) -> Area -> Rnd [(Y,X)] {- straight sections of the corridor -}+mkCorridor hv ((y0,x0),(y1,x1)) b =+ do+ (ry,rx) <- findLocInArea b (const True)+ -- (ry,rx) is intermediate point the path crosses+ -- hv decides whether we start in horizontal or vertical direction+ case hv of+ Horiz -> return [(y0,x0),(y0,rx),(y1,rx),(y1,x1)]+ Vert -> return [(y0,x0),(ry,x0),(ry,x1),(y1,x1)]++-- | Try to connect two rooms with a corridor.+-- The condition passed to mkCorridor is tricky; there might not always+-- exist a suitable intermediate point if the rooms are allowed to be close+-- together ...+connectRooms :: Area -> Area -> Rnd [Loc]+connectRooms sa@((sy0,sx0),(sy1,sx1)) ta@((ty0,tx0),(ty1,tx1)) =+ do+ (sy,sx) <- locInArea sa+ (ty,tx) <- locInArea ta+ let xok = sx1 < tx0 - 3+ let xarea = normalizeArea ((sy,sx1+2),(ty,tx0-2))+ let yok = sy1 < ty0 - 3+ let yarea = normalizeArea ((sy1+2,sx),(ty0-2,tx))+ let xyarea = normalizeArea ((sy1+2,sx1+2),(ty0-2,tx0-2))+ (hv,area) <- if xok && yok then fmap (\ hv -> (hv,xyarea)) (binaryChoice Horiz Vert)+ else if xok then return (Horiz,xarea)+ else return (Vert,yarea)+ mkCorridor hv ((sy,sx),(ty,tx)) area++-- | Actually dig a corridor.+digCorridor :: Corridor -> LMap -> LMap+digCorridor (p1:p2:ps) l =+ digCorridor (p2:ps) (M.unionWith corridorUpdate newPos l)+ where+ newPos = M.fromList [ (ps,newTile Corridor) | ps <- fromTo p1 p2 ]+ corridorUpdate _ (Tile (Wall hv) is,u) = (Tile (Opening hv) is,u)+ corridorUpdate _ (Tile (Opening hv) is,u) = (Tile (Opening hv) is,u)+ corridorUpdate _ (Tile (Floor l) is,u) = (Tile (Floor l) is,u)+ corridorUpdate (x,u) _ = (x,u)+digCorridor _ l = l++-- | Create a new tile.+newTile :: Terrain -> (Tile, Tile)+newTile t = (Tile t [], Tile Unknown [])++-- | Create a level consisting of only one room. Optionally, insert some walls.+emptyroom :: (Level -> Rnd (LMap -> LMap)) -> LevelConfig ->+ LevelName -> Rnd (Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) -> Level, Loc, Loc)+emptyroom addWallsRnd cfg@(LevelConfig { levelSize = (sy,sx) }) nm =+ do+ let lmap = digRoom Light ((1,1),(sy-1,sx-1)) (emptyLMap (sy,sx))+ let smap = M.fromList [ ((y,x),-100) | y <- [0..sy], x <- [0..sx] ]+ let lvl = Level nm (sy,sx) [] smap lmap ""+ -- locations of the stairs+ su <- findLoc lvl (const floor)+ sd <- findLoc lvl (\ l t -> floor t+ && distance (su,l) > minStairsDistance cfg)+ is <- rollItems cfg lvl su+ addWalls <- addWallsRnd lvl+ let addItem lmap (l,it) =+ M.update (\ (t,r) -> Just (t { titems = it : titems t }, r)) l lmap+ flmap lu ld =+ addWalls $+ maybe id (\ l -> M.insert su (newTile (Stairs Light Up l))) lu $+ maybe id (\ l -> M.insert sd (newTile (Stairs Light Down l))) ld $+ (\lmap -> foldl' addItem lmap is) $+ lmap+ level lu ld = Level nm (sy,sx) [] smap (flmap lu ld) "bigroom"+ return (level, su, sd)++-- | For a bigroom level: Create a level consisting of only one, empty room.+bigroom :: LevelConfig ->+ LevelName -> Rnd (Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) -> Level, Loc, Loc)+bigroom = emptyroom (\ lvl -> return id)++-- | For a noiseroom level: Create a level consisting of only one room+-- with randomly distributed pillars.+noiseroom :: LevelConfig ->+ LevelName -> Rnd (Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) -> Level, Loc, Loc)+noiseroom cfg =+ let addWalls lvl = do+ rs <- rollPillars cfg lvl+ let insertWall lmap l =+ case lmap `at` l of+ Tile (Floor _) [] -> M.insert l (newTile (Wall O)) lmap+ _ -> lmap+ return $ \ lmap -> foldl' insertWall lmap rs+ in emptyroom addWalls cfg++data LevelConfig =+ LevelConfig {+ levelGrid :: Rnd (Y,X),+ minRoomSize :: Rnd (Y,X),+ darkRoomChance :: Rnd Bool,+ border :: Int, -- must be at least 2!+ levelSize :: (Y,X), -- lower right point+ extraConnects :: (Y,X) -> Int,+ -- relative to grid+ -- (in fact a range, because of duplicate connects)+ noRooms :: (Y,X) -> Rnd Int,+ -- range, relative to grid+ minStairsDistance :: Int, -- must not be too large+ doorChance :: Rnd Bool,+ doorOpenChance :: Rnd Bool,+ doorSecretChance :: Rnd Bool,+ doorSecretMax :: Int,+ nrItems :: Rnd Int, -- range+ depth :: Int -- general indicator of difficulty+ }++normalLevelSize :: (Y,X)+normalLevelSize = (22,79)++defaultLevelConfig :: Int -> LevelConfig+defaultLevelConfig d =+ LevelConfig {+ levelGrid = do+ y <- randomR (2,4)+ x <- randomR (3,5)+ return (y,x),+ minRoomSize = return (2,2),+ darkRoomChance = chance $ 1%((22 - (2 * fromIntegral d)) `max` 2),+ border = 2,+ levelSize = normalLevelSize,+ extraConnects = \ (y,x) -> (y*x) `div` 3,+ noRooms = \ (y,x) -> randomR (0,(y*x) `div` 3),+ minStairsDistance = 676,+ doorChance = chance $ 2%3,+ doorOpenChance = chance $ 1%10,+ doorSecretChance = chance $ 1%4,+ doorSecretMax = 15,+ nrItems = randomR (5,10),+ depth = d+ }++largeLevelConfig :: Int -> LevelConfig+largeLevelConfig d =+ (defaultLevelConfig d) {+ levelGrid = return (7,10),+ levelSize = (77,231),+ extraConnects = const 10+ }++-- | Create a "normal" dungeon level. Takes a configuration in order+-- to tweak all sorts of data.+level :: LevelConfig ->+ LevelName ->+ Rnd (Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) ->+ Level, Loc, Loc)+level cfg nm =+ do+ lgrid <- levelGrid cfg+ lminroom <- minRoomSize cfg+ let gs = M.toList (grid lgrid ((0,0),levelSize cfg))+ -- grid locations of "no-rooms"+ nrnr <- noRooms cfg lgrid+ nr <- replicateM nrnr (do+ let (y,x) = lgrid+ yg <- randomR (0,y-1)+ xg <- randomR (0,x-1)+ return (yg,xg))+ rs0 <- mapM (\ (i,r) -> do+ r' <- if i `elem` nr+ then mkNoRoom (border cfg) r+ else mkRoom (border cfg) lminroom r+ return (i,r')) gs+ let rooms :: [(Loc, Loc)]+ rooms = L.map snd rs0+ dlrooms <- (mapM (\ r -> darkRoomChance cfg >>= \ c -> return (r, toDL (not c))) rooms) :: Rnd [((Loc, Loc), DL)]+ let rs = M.fromList rs0+ connects <- connectGrid lgrid+ addedConnects <- replicateM (extraConnects cfg lgrid) (randomConnection lgrid)+ let allConnects = L.nub (addedConnects ++ connects)+ cs <- mapM+ (\ (p0,p1) -> do+ let r0 = rs ! p0+ r1 = rs ! p1+ connectRooms r0 r1) allConnects+ let smap = M.fromList [ ((y,x),-100) | let (sy,sx) = levelSize cfg,+ y <- [0..sy], x <- [0..sx] ]+ let lmap :: LMap+ lmap = foldr digCorridor (foldr (\ (r, dl) m -> digRoom dl r m)+ (emptyLMap (levelSize cfg)) dlrooms) cs+ let lvl = Level nm (levelSize cfg) [] smap lmap ""+ -- convert openings into doors+ dlmap <- fmap M.fromList . mapM+ (\ o@((y,x),(t,r)) ->+ case t of+ Tile (Opening hv) _ ->+ do+ -- openings have a certain chance to be doors;+ -- doors have a certain chance to be open; and+ -- closed doors have a certain chance to be+ -- secret+ rb <- doorChance cfg+ ro <- doorOpenChance cfg+ rs <- if ro then return Nothing+ else do rsc <- doorSecretChance cfg+ fmap Just+ (if rsc then randomR (1, doorSecretMax cfg)+ else return 0)+ if rb+ then return ((y,x),newTile (Door hv rs))+ else return o+ _ -> return o) .+ M.toList $ lmap+ -- locations of the stairs+ su <- findLoc lvl (const floor)+ sd <- findLocTry 1000 lvl+ (const floor)+ (\ l t -> distance (su,l) > minStairsDistance cfg)+ -- determine number of items, items and locations for the items+ is <- rollItems cfg lvl su+ -- generate map and level from the data+ let meta = show allConnects+ return (\ lu ld ->+ let flmap = maybe id (\ l -> M.update (\ (t,r) -> Just $ newTile (Stairs (toDL $ light t) Up l)) su) lu $+ maybe id (\ l -> M.update (\ (t,r) -> Just $ newTile (Stairs (toDL $ light t) Down l)) sd) ld $+ foldr (\ (l,it) f -> M.update (\ (t,r) -> Just (t { titems = it : titems t }, r)) l . f) id is+ dlmap+ in Level nm (levelSize cfg) [] smap flmap meta, su, sd)++rollItems :: LevelConfig -> Level -> Loc -> Rnd [(Loc, Item)]+rollItems cfg lvl ploc =+ do+ nri <- nrItems cfg+ replicateM nri $+ do+ t <- newItem (depth cfg) itemFrequency+ l <- case itype t of+ Sword _ ->+ -- swords generated close to monsters; MUAHAHAHA+ findLocTry 200 lvl+ (const floor)+ (\ l t -> distance (ploc, l) > 400)+ _ -> findLoc lvl (const floor)+ return (l,t)++rollPillars :: LevelConfig -> Level -> Rnd [Loc]+rollPillars cfg lvl =+ do+ nri <- 100 *~ nrItems cfg+ replicateM nri $+ do+ l <- findLoc lvl (const floor)+ return l++emptyLMap :: (Y,X) -> LMap+emptyLMap (my,mx) = M.fromList [ ((y,x),newTile Rock) | x <- [0..mx], y <- [0..my] ]++-- | If the room has size 1, it is assumed to be a no-room, and a single+-- corridor field will be dug instead of a room.+digRoom :: DL -> Room -> LMap -> LMap+digRoom dl ((y0,x0),(y1,x1)) l+ | y0 == y1 && x0 == x1 =+ M.insert (y0,x0) (newTile Corridor) l+ | otherwise =+ let rm = M.fromList $ [ ((y,x),newTile (Floor dl)) | x <- [x0..x1], y <- [y0..y1] ]+ ++ [ ((y,x),newTile (Wall p)) | (x,y,p) <- [(x0-1,y0-1,UL),(x1+1,y0-1,UR),(x0-1,y1+1,DL),(x1+1,y1+1,DR)] ]+ ++ [ ((y,x),newTile (Wall p)) | x <- [x0..x1], (y,p) <- [(y0-1,U),(y1+1,D)] ]+ ++ [ ((y,x),newTile (Wall p)) | (x,p) <- [(x0-1,L),(x1+1,R)], y <- [y0..y1] ]+ in M.unionWith const rm l++-- | Create a new monster in the level, at a random position.+addMonster :: Level -> Player -> Rnd Level+addMonster lvl@(Level { lmonsters = ms, lmap = lmap })+ player@(Monster { mloc = ploc }) =+ do+ rc <- monsterGenChance (lname lvl) ms+ if rc+ then do+ -- TODO: new monsters should always be generated in a place that isn't+ -- visible by the player (if possible -- not possible for bigrooms)+ -- levels with few rooms are dangerous, because monsters may spawn+ -- in adjacent and unexpected places+ sm <- findLocTry 1000 lvl+ (\ l t -> not (l `L.elem` L.map mloc (player : ms))+ && open t)+ (\ l t -> distance (ploc, l) > 400+ && floor t)+ m <- newMonster sm monsterFrequency+ return (updateMonsters (const (m : ms)) lvl)+ else return lvl
+ src/FOV.hs view
@@ -0,0 +1,414 @@+module FOV where++import Data.Map as M+import Data.Set as S+import Data.List as L+import Data.Ratio+import Data.Maybe+import Debug.Trace++import Geometry+import Level++type Interval = (Rational, Rational)+type Distance = Int+type Progress = Int+newtype Bump = B Loc deriving (Show)+type Line = (Bump, Bump)+type ConvexHull = [Bump]+type Edge = (Line, ConvexHull)+type EdgeInterval = (Edge, Edge)++data FovMode = Shadow | Permissive Int | Diagonal Int++-- Three Field of View algorithms. Press 'V' to cycle among them in the game.++-- The main FOV function.++-- | Perform a full scan for a given location. Returns the locations+-- that are currently in the field of view.+fullscan :: FovMode -> Loc -> LMap -> Set Loc+fullscan fovMode loc lmap =+ case fovMode of+ Shadow -> -- shadow casting with infinite range+ S.unions $+ L.map (\ tr ->+ scan (tr loc) lmap 1 (0,1)) -- was: scan (tr loc) lmap 0 (0,1); TODO: figure out what difference this makes+ [tr0,tr1,tr2,tr3,tr4,tr5,tr6,tr7]+ Permissive r -> -- permissive with range r+ S.unions $+ L.map (\ tr ->+ pscan r (tr loc) lmap 1+ (((B(1, 0), B(0, r+1)), [B(0, 1)]),+ ((B(0, 1), B(r+1, 0)), [B(1, 0)])))+ [qtr0,qtr1,qtr2,qtr3]+ Diagonal r -> -- diagonal with range r+ S.unions $+ L.map (\ tr ->+ dscan r (tr loc) lmap 1+ (((B(0, 1), B(r, -r)), [B(0, 0)]),+ ((B(0, 0), B(r, r+1)), [B(0, 1)])))+ [qtr0,qtr1,qtr2,qtr3]++-- Common functions.++-- | The translation, rotation and symmetry functions for octants.+tr0 (oy,ox) (d,p) = (oy + d,ox + p)+tr1 (oy,ox) (d,p) = (oy + d,ox - p)+tr2 (oy,ox) (d,p) = (oy - d,ox + p)+tr3 (oy,ox) (d,p) = (oy - d,ox - p)+tr4 (oy,ox) (d,p) = (oy + p,ox + d)+tr5 (oy,ox) (d,p) = (oy + p,ox - d)+tr6 (oy,ox) (d,p) = (oy - p,ox + d)+tr7 (oy,ox) (d,p) = (oy - p,ox - d)++-- | The translation and rotation functions for quadrants.+qtr0, qtr1, qtr2, qtr3 :: Loc -> Bump -> Loc+qtr0 (oy, ox) (B(y, x)) = (oy - y, ox + x) -- first quadrant+qtr1 (oy, ox) (B(y, x)) = (oy - x, ox - y) -- then rotated clockwise 90 degrees+qtr2 (oy, ox) (B(y, x)) = (oy + y, ox - x)+qtr3 (oy, ox) (B(y, x)) = (oy + x, ox + y)++-- | Integer division, rounding up.+divUp n k = - (-n) `div` k++-- | Maximal element of a non-empty list. Prefers elements from the rear,+-- which is essential for PFOV, to avoid ill-defined lines.+maximal :: (a -> a -> Bool) -> [a] -> a+maximal gte = L.foldl1' (\ acc x -> if gte x acc then x else acc)++-- | Check if the line from the second point to the first is more steep+-- than the line from the third point to the first. This is related+-- to the formal notion of gradient (or angle), but hacked wrt signs+-- to work in this particular setup. Returns True for ill-defined lines.+steeper :: Bump -> Bump -> Bump -> Bool+steeper (B(yf, xf)) (B(y1, x1)) (B(y2, x2)) =+ (yf - y1)*(xf - x2) >= (yf - y2)*(xf - x1)++-- | Adds a bump to the convex hull of bumps represented as a list.+addHull :: (Bump -> Bump -> Bool) -> Bump -> ConvexHull -> ConvexHull+addHull gte b l =+ case l of+ x:y:zs ->+ if gte x y+ then addHull gte b (y:zs)+ else b : l+ _ -> b : l++-- Recursive Shadow Casting.++-- | A restrictive variant of Recursive Shadow Casting FOV with infinite range.+-- It's not designed for dungeons with diagonal walls, so they block visibility,+-- though they don't block movement. Such cases appear in LambdaHack only+-- when two corridors touch diagonally by accident and on the random pillars+-- levels.++-- | The current state of a scan is kept in a variable of Maybe Rational.+-- If Just something, we're in a visible interval. If Nothing, we're in+-- a shadowed interval.+scan :: ((Distance, Progress) -> Loc) -> LMap -> Distance -> Interval -> Set Loc+scan tr l d (s,e) =+ let ps = downBias (s * fromIntegral d) -- minimal progress to check+ pe = upBias (e * fromIntegral d) -- maximal progress to check+ st = if open (l `at` tr (d,ps)) then Just s -- start in light+ else Nothing -- start in shadow+ in+ -- trace (show (d,s,e,ps,pe)) $+ S.union (S.fromList [tr (d,p) | p <- [ps..pe]]) (scan' st ps pe)+ where+ scan' :: Maybe Rational -> Progress -> Progress -> Set Loc+ -- scan' st ps pe+ -- | trace (show (st,ps,pe)) False = undefined+ scan' (Just s) ps pe+ | s >= e = S.empty -- empty interval+ | ps > pe = scan tr l (d+1) (s,e) -- reached end, scan next+ | closed (l `at` tr (d,ps)) =+ let ne = (fromIntegral ps - (1%2)) / (fromIntegral d + (1%2))+ in scan tr l (d+1) (s,ne) `S.union` scan' Nothing (ps+1) pe+ -- entering shadow+ | otherwise = scan' (Just s) (ps+1) pe+ -- continue in light+ scan' Nothing ps pe+ | ps > pe = S.empty -- reached end while in shadow+ | open (l `at` tr (d,ps)) =+ let ns = (fromIntegral ps - (1%2)) / (fromIntegral d - (1%2))+ in scan' (Just ns) (ps+1) pe+ -- moving out of shadow+ | otherwise = scan' Nothing (ps+1) pe+ -- continue in shadow++downBias, upBias :: (Integral a, Integral b) => Ratio a -> b+downBias x = round (x - 1 % (denominator x * 3))+upBias x = round (x + 1 % (denominator x * 3))+++-- Permissive FOV with a given range.++-- | PFOV, clean-room reimplemented based on the algorithm described in http://roguebasin.roguelikedevelopment.org/index.php?title=Precise_Permissive_Field_of_View,+-- though the general structure is more influenced by recursive shadow casting,+-- as implemented above. In the result, this algorithm is much faster+-- on dense maps, since it does not scan areas blocked by shadows.+-- See https://github.com/Mikolaj/LambdaHack/wiki/Fov-and-los for some more context.++-- TODO: Scanning squares on horizontal lines in octants, not squares+-- on diagonals in quadrants, may be much faster and a bit simpler.+-- Right now we build new view on each end of each visible wall tile+-- and this is necessary only for straight, thin, diagonal walls.++-- | The current state of a scan is kept in Maybe (Line, ConvexHull).+-- If Just something, we're in a visible interval. If Nothing, we're in+-- a shadowed interval.+pscan :: Distance -> (Bump -> Loc) -> LMap -> Distance -> EdgeInterval ->+ Set Loc+pscan r ptr l d (s@(sl{-shallow line-}, sBumps), e@(el{-steep line-}, eBumps)) =+ -- trace (show (d,s,e,ps,pe)) $+ if illegal+ then S.empty+ else S.union outside (S.fromList [tr (d, p) | p <- [ps..pe]])+ -- the area is diagonal, which is incorrect, but looks good enough+ where+ (ns, ks) = pintersect sl d+ (ne, ke) = pintersect el d+ -- Corners are translucent, so they are invisible, so if intersection+ -- is at a corner, choose pe that creates the smaller view.+ (ps, pe) = (ns `div` ks, ne `divUp` ke - 1) -- progress interval to check+ -- Single ray from an extremity, produces non-permissive digital lines.+ illegal = let (n, k) = pintersect sl 0+ in ns*ke == ne*ks && (n == 0 || n == k)+ outside+ | d >= r = S.empty+ | open (l `at` tr (d, ps)) = pscan' (Just s) ps -- start in light+ | ps == ns `divUp` ks = pscan' (Just s) ps -- start in a corner+ | otherwise = pscan' Nothing (ps+1) -- start in mid-wall++ dp2bump (d, p) = B(p, d - p)+ bottomRight (d, p) = B(p, d - p + 1)+ tr = ptr . dp2bump++ pscan' :: Maybe Edge -> Progress -> Set Loc+ pscan' (Just s@(_, sBumps)) ps+ | ps > pe = -- reached end, scan next+ pscan r ptr l (d+1) (s, e)+ | closed (l `at` tr (d, ps)) = -- entering shadow, steep bump+ let steepBump = bottomRight (d, ps)+ gte = flip $ psteeper steepBump+ -- sBumps may contain steepBump, but maximal will ignore it+ nep = maximal gte sBumps+ neBumps = addHull gte steepBump eBumps+ in S.union+ (pscan r ptr l (d+1) (s, (pline nep steepBump, neBumps)))+ (pscan' Nothing (ps+1))+ | otherwise = pscan' (Just s) (ps+1) -- continue in light++ pscan' Nothing ps+ | ps > ne `div` ke = S.empty -- reached absolute end+ | otherwise = -- out of shadow, shallow bump+ -- the light can be just through a corner of diagonal walls+ -- and the recursive call verifies that at the same ps coordinate+ let shallowBump = bottomRight (d, ps)+ gte = psteeper shallowBump+ nsp = maximal gte eBumps+ nsBumps = addHull gte shallowBump sBumps+ in pscan' (Just (pline nsp shallowBump, nsBumps)) ps++ pline p1 p2 =+ pdebugLine $ -- TODO: disable when it becomes a bottleneck+ (p1, p2)++ psteeper f p1 p2 =+ pdebugSteeper f p1 p2 $ -- TODO: disable when it becomes a bottleneck+ steeper f p1 p2++-- | The y coordinate, represented as a fraction, of the intersection of+-- a given line and the line of diagonals of squares at distance d from (0, 0).+pintersect :: Line -> Distance -> (Int, Int)+pintersect (B(y, x), B(yf, xf)) d =+ ((1 + d)*(yf - y) + y*xf - x*yf, (xf - x) + (yf - y))+{-+Derivation of the formula:+The intersection point (yt, xt) satisfies the following equalities:+xt = 1 + d - yt+(yt - y) (xf - x) = (xt - x) (yf - y)+hence+(yt - y) (xf - x) = (xt - x) (yf - y)+yt (xf - x) - y xf = xt (yf - y) - x yf+yt (xf - x) - y xf = (1 + d) (yf - y) - yt (yf - y) - x yf+yt (xf - x) + yt (yf - y) = (1 + d) (yf - y) - x yf + y xf+yt = ((1 + d) (yf - y) + y xf - x yf) / (xf - x + yf - y)++General remarks:+A square is denoted by its bottom-left corner. Hero at (0,0).+Order of processing in the first quadrant is+9+58+247+@136+so the first processed square is at (0, 1). The order is reversed+wrt the shadow casting algorithm above. The line in the curent state+of scan' is not the steep line, but the shallow line,+and we start scanning from the bottom right.++The Loc coordinates are cartesian, the Bump coordinates are cartesian,+translated so that the hero is at (0, 0) and rotated so that he always+looks at the first quadrant, the (Distance, Progress) cordinates+are mangled and not used for geometry.+-}++-- | Debug functions for PFOV:++-- | Debug: calculate steeper for PFOV in another way and compare results.+pdebugSteeper :: Bump -> Bump -> Bump -> Bool -> Bool+pdebugSteeper f p1 p2 x =+ let (n1, k1) = pintersect (p1, f) 0+ (n2, k2) = pintersect (p2, f) 0+ in if x == (n1 * k2 <= k1 * n2)+ then x+ else error $ "psteeper: " ++ show (f, p1, p2, x)++-- | Debug: checks postconditions of borderLine.+pdebugLine :: Line -> Line+pdebugLine line@(B(y1, x1), B(y2, x2))+ | y1 == y2 && x1 == x2 =+ error $ "pdebugLine: wrongly defined line " ++ show line+ | x2 - x1 == - (y2 - y1) =+ error $ "pdebugLine: diagonal line " ++ show line+ | crossL0 =+ error $ "pdebugLine: crosses diagonal below 0 " ++ show line+ | crossG1 =+ error $ "pdebugLine: crosses diagonal above 1 " ++ show line+ | otherwise = line+ where+ (n, k) = pintersect line 0+ (q, r) = if k == 0 then (0, 0) else n `divMod` k+ crossL0 = q < 0 -- q truncated toward negative infinity+ crossG1 = q >= 1 && (q > 1 || r /= 0)+++-- Digital FOV with a given range.++-- | DFOV, according to specification at http://roguebasin.roguelikedevelopment.org/index.php?title=Digital_field_of_view_implementation,+-- but AFAIK, this algorithm (fast DFOV done similarly as PFOV) has never been+-- implemented before. The algorithm is based on the PFOV algorithm,+-- clean-room reimplemented based on http://roguebasin.roguelikedevelopment.org/index.php?title=Precise_Permissive_Field_of_View.+-- See https://github.com/Mikolaj/LambdaHack/wiki/Fov-and-los for some more context.++-- | The current state of a scan is kept in Maybe (Line, ConvexHull).+-- If Just something, we're in a visible interval. If Nothing, we're in+-- a shadowed interval.+dscan :: Distance -> (Bump -> Loc) -> LMap -> Distance -> EdgeInterval ->+ Set Loc+dscan r tr l d (s@(sl{-shallow line-}, sBumps), e@(el{-steep line-}, eBumps)) =+ -- trace (show (d,s,e,ps,pe)) $+ S.union outside (S.fromList [tr (B(d, p)) | p <- [ps..pe]])+ -- the scanned area is a square, which is a sphere in this metric; good+ where+ ps = let (n, k) = dintersect sl d -- minimal progress to check+ in n `div` k+ pe = let (n, k) = dintersect el d -- maximal progress to check+ -- Corners obstruct view, so the steep line, constructed+ -- from corners, is itself not a part of the view,+ -- so if its intersection with the line of diagonals is only+ -- at a corner, choose the diamond leading to a smaller view.+ in -1 + n `divUp` k+ outside+ | d >= r = S.empty+ | ps > pe = error $ "dscan: wrong start " ++ show (d, (ps, pe))+ | open (l `at` tr (B(d, ps))) =+ dscan' (Just s) (ps+1) -- start in light, jump ahead+ | otherwise = dscan' Nothing (ps+1) -- start in shadow, jump ahead++ dscan' :: Maybe Edge -> Progress -> Set Loc+ dscan' (Just s@(_, sBumps)) ps+ | ps > pe = dscan r tr l (d+1) (s, e) -- reached end, scan next+ | closed (l `at` tr steepBump) = -- entering shadow+ S.union+ (dscan r tr l (d+1) (s, (dline nep steepBump, neBumps)))+ (dscan' Nothing (ps+1))+ | otherwise = dscan' (Just s) (ps+1) -- continue in light+ where+ steepBump = B(d, ps)+ gte = dsteeper steepBump+ nep = maximal gte sBumps+ neBumps = addHull gte steepBump eBumps++ dscan' Nothing ps+ | ps > pe = S.empty -- reached end while in shadow+ | open (l `at` tr shallowBump) = -- moving out of shadow+ dscan' (Just (dline nsp shallowBump, nsBumps)) (ps+1)+ | otherwise = dscan' Nothing (ps+1) -- continue in shadow+ where+ shallowBump = B(d, ps)+ gte = flip $ dsteeper shallowBump+ nsp = maximal gte eBumps+ nsBumps = addHull gte shallowBump sBumps++ dline p1 p2 =+ ddebugLine $ -- TODO: disable when it becomes a bottleneck+ (p1, p2)++ dsteeper f p1 p2 =+ ddebugSteeper f p1 p2 $ -- TODO: disable when it becomes a bottleneck+ steeper f p1 p2++-- | The x coordinate, represented as a fraction, of the intersection of+-- a given line and the line of diagonals of diamonds at distance d from (0, 0).+dintersect :: Line -> Distance -> (Int, Int)+dintersect (B(y, x), B(yf, xf)) d =+ ((d - y)*(xf - x) + x*(yf - y), yf - y)+{-+Derivation of the formula:+The intersection point (yt, xt) satisfies the following equalities:+yt = d+(yt - y) (xf - x) = (xt - x) (yf - y)+hence+(yt - y) (xf - x) = (xt - x) (yf - y)+(d - y) (xf - x) = (xt - x) (yf - y)+(d - y) (xf - x) + x (yf - y) = xt (yf - y)+xt = ((d - y) (xf - x) + x (yf - y)) / (yf - y)++General remarks:+A diamond is denoted by its left corner. Hero at (0,0).+Order of processing in the first quadrant rotated by 45 degrees is+ 45678+ 123+ @+so the first processed diamond is at (-1, 1). The order is similar+as for the shadow casting algorithm above and reversed wrt PFOV.+The line in the curent state of scan' is called the shallow line,+but it's the one that delimits the view from the left, while the steep+line is on the right, opposite to PFOV. We start scanning from the left.++The Loc coordinates are cartesian, the Bump coordinates are cartesian,+translated so that the hero is at (0, 0) and rotated so that he always+looks at the first roatated quadrant, the (Distance, Progress) cordinates+coincide with the Bump coordinates, unlike in PFOV.+-}++-- | Debug functions for DFOV:++-- | Debug: calculate steeper for DFOV in another way and compare results.+ddebugSteeper :: Bump -> Bump -> Bump -> Bool -> Bool+ddebugSteeper f p1 p2 x =+ let (n1, k1) = dintersect (p1, f) 0+ (n2, k2) = dintersect (p2, f) 0+ in if x == (n1 * k2 >= k1 * n2)+ then x+ else error $ "dsteeper: " ++ show (f, p1, p2, x)++-- | Debug: check is a view border line for DFOV is legal.+ddebugLine :: Line -> Line+ddebugLine line@(B(y1, x1), B(y2, x2))+ | y1 == y2 && x1 == x2 =+ error $ "ddebugLine: wrongly defined line " ++ show line+ | y2 - y1 == 0 =+ error $ "ddebugLine: horizontal line " ++ show line+ | crossL0 =+ error $ "ddebugLine: crosses the X axis below 0 " ++ show line+ | crossG1 =+ error $ "ddebugLine: crosses the X axis above 1 " ++ show line+ | otherwise = line+ where+ (n, k) = dintersect line 0+ (q, r) = if k == 0 then (0, 0) else n `divMod` k+ crossL0 = q < 0 -- q truncated toward negative infinity+ crossG1 = q >= 1 && (q > 1 || r /= 0)
+ src/File.hs view
@@ -0,0 +1,22 @@+module File where++import System.IO+import Data.Binary+import qualified Data.ByteString.Lazy as LBS+import Codec.Compression.Zlib as Z++strictReadCompressedFile :: FilePath -> IO LBS.ByteString+strictReadCompressedFile f =+ do+ h <- openBinaryFile f ReadMode+ c <- LBS.hGetContents h+ let d = Z.decompress c+ LBS.length d `seq` return d++strictDecodeCompressedFile :: Binary a => FilePath -> IO a+strictDecodeCompressedFile = fmap decode . strictReadCompressedFile++encodeCompressedFile :: Binary a => FilePath -> a -> IO ()+encodeCompressedFile f = LBS.writeFile f . Z.compress . encode+ -- note that LBS.writeFile opens the file in binary mode+
+ src/Frequency.hs view
@@ -0,0 +1,31 @@+module Frequency where++import Control.Monad++newtype Frequency a = Frequency { runFrequency :: [(Int, a)] }+ deriving Show++instance Monad Frequency where+ return x = Frequency [(1, x)]+ m >>= f = Frequency+ [(p * q, y) | (p, x) <- runFrequency m, + (q, y) <- runFrequency (f x) ]+ fail "" = Frequency []++instance MonadPlus Frequency where+ mplus (Frequency xs) (Frequency ys) = Frequency (xs ++ ys)+ mzero = Frequency []++instance Functor Frequency where+ fmap f (Frequency xs) = Frequency (map (\ (p, x) -> (p, f x)) xs)++-- only try the second possibility if the first fails+melse :: Frequency a -> Frequency a -> Frequency a+melse (Frequency []) y = y+melse x y = x++scale :: Int -> Frequency a -> Frequency a+scale n (Frequency xs) = Frequency (map (\ (p, x) -> (n * p, x)) xs)++uniform :: [a] -> Frequency a+uniform = Frequency . map (\ x -> (1, x))
+ src/Geometry.hs view
@@ -0,0 +1,71 @@+module Geometry where++-- | Game time in turns. (Placement in module Geometry is not ideal.)+type Time = Int++type X = Int+type Y = Int++type Loc = (Y,X)+type Dir = (Y,X)+type Area = ((Y,X),(Y,X))++-- | Given two locations, determine the direction in which one should+-- move from the first in order to get closer to the second. Does not+-- pay attention to obstacles at all.+towards :: (Loc,Loc) -> Dir+towards ((y0,x0),(y1,x1)) =+ let dy = y1 - y0+ dx = x1 - x0+ angle = atan (fromIntegral dy / fromIntegral dx) / (pi / 2)+ dir | angle <= -0.75 = (-1,0)+ | angle <= -0.25 = (-1,1)+ | angle <= 0.25 = (0,1)+ | angle <= 0.75 = (1,1)+ | angle <= 1.25 = (1,0)+ | otherwise = (0,0)+ in if dx >= 0 then dir else neg dir++-- | Get the squared distance between two locations.+distance :: (Loc,Loc) -> Int+distance ((y0,x0),(y1,x1)) = (y1 - y0)^2 + (x1 - x0)^2++-- | Return whether two locations are adjacent on the map+-- (horizontally, vertically or diagonally). Currrently, a+-- position is also considered adjacent to itself.+adjacent :: Loc -> Loc -> Bool+adjacent s t = distance (s,t) <= 2++-- | Return the 8 surrounding locations of a given location.+surroundings :: Loc -> [Loc]+surroundings l = map (l `shift`) moves++diagonal :: Dir -> Bool+diagonal (y,x) = y*x /= 0++-- | Move one square in the given direction.+shift :: Loc -> Dir -> Loc+shift (y0,x0) (y1,x1) = (y0+y1,x0+x1)++-- | Invert a direction (vector).+neg :: Dir -> Dir+neg (y,x) = (-y,-x)++-- | Get the vectors of all the moves.+moves :: [Dir]+moves = [ (x,y) | x <- [-1..1], y <- [-1..1], x /= 0 || y /= 0 ]++up, down, left, right :: Dir+upleft, upright, downleft, downright :: Dir+upleft = up `shift` left+upright = up `shift` right+downleft = down `shift` left+downright = down `shift` right+up = (-1,0)+down = (1,0)+left = (0,-1)+right = (0,1)++horiz, vert :: [Dir]+horiz = [left, right]+vert = [up, down]
+ src/Grammar.hs view
@@ -0,0 +1,56 @@+module Grammar where++import Data.Char++import Item+import Monster+import State+import ItemState++-- | How to refer to a monster in object position of a sentence.+objectMonster :: MonsterType -> String+objectMonster Player = "you"+objectMonster Eye = "the reducible eye"+objectMonster FastEye = "the super-fast eye"+objectMonster Nose = "the point-free nose"++-- | How to refer to a monster in subject position of a sentence.+subjectMonster :: MonsterType -> String+subjectMonster x = let (s:r) = objectMonster x in toUpper s : r++verbMonster :: MonsterType -> String -> String+verbMonster Player v = v+verbMonster _ v = v ++ "s"++compoundVerbMonster :: MonsterType -> String -> String -> String+compoundVerbMonster Player v p = v ++ " " ++ p+compoundVerbMonster _ v p = v ++ "s " ++ p++objectItem :: State -> Int -> ItemType -> String+objectItem _ n Ring = makeObject n id "ring"+objectItem _ n Scroll = makeObject n id "scroll"+objectItem s n (Potion t) = makeObject n (identified (sassocs s) (sdiscoveries s) (Potion t)) "potion"+objectItem _ n Wand = makeObject n id "wand"+objectItem _ n Amulet = makeObject n id "amulet"+objectItem _ n Gem = makeObject n id "gem"+objectItem _ n Gold = makeObject n id "gold piece"+objectItem _ n (Sword i) = makeObject n id ("(+" ++ show i ++ ") sword")++subjectVerbIObject :: State -> Monster -> String -> Item -> String -> String+subjectVerbIObject state m v o add =+ subjectMonster (mtype m) ++ " " +++ verbMonster (mtype m) v ++ " " +++ objectItem state (icount o) (itype o) ++ add ++ "."++subjectVerbMObject :: State -> Monster -> String -> Monster -> String -> String+subjectVerbMObject state m v o add =+ subjectMonster (mtype m) ++ " " +++ verbMonster (mtype m) v ++ " " +++ objectMonster (mtype o) ++ add ++ "."++subjectCompoundVerbIObject :: State -> Monster -> String -> String ->+ Item -> String -> String+subjectCompoundVerbIObject state m v p o add =+ subjectMonster (mtype m) ++ " " +++ compoundVerbMonster (mtype m) v p ++ " " +++ objectItem state (icount o) (itype o) ++ add ++ "."
+ src/HighScores.hs view
@@ -0,0 +1,147 @@+module HighScores where++import System.Directory+import Control.Exception as E hiding (handle)+import Text.Printf+import System.Time++import Data.Binary+import Data.List as L+import Data.Maybe++import File+import Dungeon+import qualified Config++-- | A single score.+-- TODO: add hero's name, exp and level, cause of death, user number/name.+-- Note: I tried using Date.Time, but got all kinds of problems,+-- including build problems and opaque types that make serialization difficult,+-- and I couldn't use Datetime because it needs old base (and is under GPL).+-- TODO: When we finally move to Date.Time, let's take timezone into account.+data ScoreRecord = ScoreRecord+ { points :: Int,+ negTurn :: Int,+ date :: ClockTime,+ current :: Int,+ killed :: Bool,+ victor :: Bool}+ deriving (Eq, Ord)++instance Binary ClockTime where+ put (TOD s p) =+ do+ put s+ put p+ get =+ do+ s <- get+ p <- get+ return (TOD s p)++instance Binary ScoreRecord where+ put (ScoreRecord points negTurn date current killed victor) =+ do+ put points+ put negTurn+ put date+ put current+ put killed+ put victor+ get =+ do+ points <- get+ negTurn <- get+ date <- get+ current <- get+ killed <- get+ victor <- get+ return (ScoreRecord points negTurn date current killed victor)++-- | Show a single high score.+showScore :: (Int, ScoreRecord) -> String+showScore (pos, score) =+ let won = if victor score+ then "emerged victorious"+ else "is camping on level " ++ show (current score) ++ ","+ died = if killed score+ then "perished on level " ++ show (current score) ++ ","+ else won+ time = calendarTimeToString . toUTCTime . date $ score+ big = " "+ lil = " "+ in+ printf "%s\n%4d. %6d This hero %s after %d steps \n%son %s. \n"+ big pos (points score) died (- (negTurn score)) lil time++-- | The list of scores, in decreasing order.+type ScoreTable = [ScoreRecord]++-- | Empty score table+empty :: ScoreTable+empty = []++-- | Name of the high scores file.+file :: Config.CP -> IO String+file config = Config.getFile config "LambdaHack.scores" "files" "highscores"++-- | We save a simple serialized version of the high scores table.+-- The 'False' is used only as an EOF marker.+save :: Config.CP -> ScoreTable -> IO ()+save config scores =+ do+ f <- file config+ E.catch (removeFile f) (\ e -> case e :: IOException of _ -> return ())+ encodeCompressedFile f (scores, False)++-- | Read the high scores table. Return the empty table if no file.+restore :: Config.CP -> IO ScoreTable+restore config =+ E.catch (do+ f <- file config+ (x, z) <- strictDecodeCompressedFile f+ (z :: Bool) `seq` return x)+ (\ e -> case e :: IOException of+ _ -> return [])++-- | Insert a new score into the table, Return new table and the position.+insertPos :: ScoreRecord -> ScoreTable -> (ScoreTable, Int)+insertPos s h =+ let (prefix, suffix) = L.span (\ x -> x > s) h in+ (prefix ++ [s] ++ suffix, L.length prefix + 1)++-- | Show a screenful of the high scores table.+-- Parameter height is the number of (3-line) scores to be shown.+showTable :: ScoreTable -> Int -> Int -> String+showTable h start height =+ let zipped = zip [1..] h+ screenful = take height . drop (start - 1) $ zipped+ in+ L.concatMap showScore screenful++-- | Produces a couple of renderings of the high scores table.+slideshow :: Int -> ScoreTable -> Int -> [String]+slideshow pos h height =+ if pos <= height+ then [showTable h 1 height]+ else [showTable h 1 height,+ showTable h (max (height + 1) (pos - height `div` 2)) height]++-- | Take care of a new score, return a list of messages to display.+register :: Config.CP -> Bool -> ScoreRecord -> IO (String, [String])+register config write s =+ do+ h <- restore config+ let (h', pos) = insertPos s h+ (lines, _) = normalLevelSize+ height = lines `div` 3+ (msgCurrent, msgUnless) =+ if killed s+ then (" short-lived", " (score halved)")+ else if victor s+ then (" glorious",+ if pos <= height then " among the greatest heroes" else "")+ else (" current", " (unless you are slain)")+ msg = printf "Your%s exploits award you place >> %d <<%s." msgCurrent pos msgUnless+ if write then save config h' else return ()+ return (msg, slideshow pos h' height)
+ src/Item.hs view
@@ -0,0 +1,255 @@+module Item where++import Data.Binary+import Data.Map as M+import Data.Set as S+import Data.List as L+import Data.Maybe+import Data.Char+import Data.Function+import Control.Monad++import Display+import Geometry+import Random++data Item = Item+ { icount :: Int,+ itype :: ItemType,+ iletter :: Maybe Char } -- inventory identifier+ deriving Show++data ItemType =+ Ring+ | Scroll+ | Potion PotionType+ | Wand+ | Amulet+ | Gem+ | Gold+ | Sword Int+ deriving (Eq, Ord, Show)++data PotionType =+ PotionWater+ | PotionHealing+ deriving (Eq, Ord, Show)++data Appearance =+ Clear+ | White+ deriving (Eq, Show)++type Assocs = M.Map ItemType Appearance+type Discoveries = S.Set ItemType++equalItemType :: Item -> Item -> Bool+equalItemType = (==) `on` itype++equalItemLetter :: Item -> Item -> Bool+equalItemLetter = (==) `on` iletter++potionType :: PotionType -> String -> String+potionType PotionWater s = s ++ " of water"+potionType PotionHealing s = s ++ " of healing"++appearance :: Appearance -> String -> String+appearance Clear s = "clear " ++ s+appearance White s = "white " ++ s++instance Binary Item where+ put (Item icount itype iletter) = put icount >> put itype >> put iletter+ get = liftM3 Item get get get++instance Binary ItemType where+ put Ring = putWord8 0+ put Scroll = putWord8 1+ put (Potion t) = putWord8 2 >> put t+ put Wand = putWord8 3+ put Amulet = putWord8 4+ put Gem = putWord8 5+ put Gold = putWord8 6+ put (Sword i) = putWord8 7 >> put i+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Ring+ 1 -> return Scroll+ 2 -> liftM Potion get+ 3 -> return Wand+ 4 -> return Amulet+ 5 -> return Gem+ 6 -> return Gold+ 7 -> liftM Sword get++instance Binary PotionType where+ put PotionWater = putWord8 0+ put PotionHealing = putWord8 1+ get = do+ tag <- getWord8+ case tag of+ 0 -> return PotionWater+ 1 -> return PotionHealing++instance Binary Appearance where+ put Clear = putWord8 0+ put White = putWord8 1+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Clear+ 1 -> return White++itemFrequency :: Frequency ItemType+itemFrequency =+ Frequency+ [+ (100, Gold),+ (70, Sword (-1)),+ (30, Gem),+ (20, Ring),+ (30, Scroll),+ (10, Wand),+ (10, Amulet),+ (30, Potion PotionWater),+ (20, Potion PotionHealing)+ ]++itemQuantity :: Int -> ItemType -> Rnd Int+itemQuantity n Gold = (2 * n) *~ d 8+itemQuantity _ _ = return 1++itemStrength :: Int -> ItemType -> Rnd ItemType+itemStrength n (Sword _) =+ do+ r <- d (2 + n `div` 2)+ return $ Sword $ (n + 1) `div` 3 + r+itemStrength _ tp = return tp++itemLetter :: ItemType -> Maybe Char+itemLetter Gold = Just '$'+itemLetter _ = Nothing++-- | Generate an item.+newItem :: Int -> Frequency ItemType -> Rnd Item+newItem n ftp =+ do+ tp <- frequency ftp+ item <- itemStrength n tp+ nr <- itemQuantity n tp+ return (Item nr item (itemLetter tp))++-- | Assigns a letter to an item, for inclusion+-- in the inventory of the player. Takes a remembered+-- letter and a starting letter.+assignLetter :: Maybe Char -> Char -> [Item] -> Maybe Char+assignLetter r c is =+ case r of+ Just l | l `L.elem` allowed -> Just l+ _ -> listToMaybe free+ where+ current = S.fromList (concatMap (maybeToList . iletter) is)+ allLetters = ['a'..'z'] ++ ['A'..'Z']+ candidates = take (length allLetters) (drop (fromJust (L.findIndex (==c) allLetters)) (cycle allLetters))+ free = L.filter (\x -> not (x `S.member` current)) candidates+ allowed = '$' : free++cmpLetter :: Char -> Char -> Ordering+cmpLetter x y = compare (isUpper x, toLower x) (isUpper y, toLower y)++cmpLetter' :: Maybe Char -> Maybe Char -> Ordering+cmpLetter' Nothing Nothing = EQ+cmpLetter' Nothing (Just _) = GT+cmpLetter' (Just _) Nothing = LT+cmpLetter' (Just l) (Just l') = cmpLetter l l'++maxBy :: (a -> a -> Ordering) -> a -> a -> a+maxBy cmp x y = case cmp x y of+ LT -> y+ _ -> x++maxLetter = maxBy cmpLetter++mergeLetter :: Maybe Char -> Maybe Char -> Maybe Char+mergeLetter = mplus++letterRange :: [Char] -> String+letterRange xs = sectionBy (sortBy cmpLetter xs) Nothing+ where+ succLetter c d = ord d - ord c == 1++ sectionBy [] Nothing = ""+ sectionBy [] (Just (c,d)) = finish (c,d)+ sectionBy (x:xs) Nothing = sectionBy xs (Just (x,x))+ sectionBy (x:xs) (Just (c,d)) | succLetter d x+ = sectionBy xs (Just (c,x))+ | otherwise+ = finish (c,d) ++ sectionBy xs (Just (x,x))++ finish (c,d) | c == d = [c]+ | succLetter c d = [c,d]+ | otherwise = [c,'-',d]++letterLabel :: Maybe Char -> String+letterLabel Nothing = " "+letterLabel (Just c) = c : " - "++viewItem :: ItemType -> Assocs -> (Char, Attr -> Attr)+viewItem i a = viewItem' i (M.lookup i a)+ where+ viewItem' (Sword {}) _ = (')', id)+ viewItem' Ring _ = ('=', id)+ viewItem' Scroll _ = ('?', id)+ viewItem' (Potion {}) (Just Clear) = ('!', setBold . setFG blue)+ viewItem' (Potion {}) (Just White) = ('!', setBold . setFG white)+ viewItem' (Potion {}) _ = ('!', id)+ viewItem' Wand _ = ('/', id)+ viewItem' Gold _ = ('$', setBold . setFG yellow)+ viewItem' Gem _ = ('*', setFG red)+ viewItem' Amulet _ = ('"', id)+ viewItem' _ _ = ('~', id)++-- | Adds an item to a list of items, joining equal items.+-- Also returns the joined item.+joinItem :: Item -> [Item] -> (Item,[Item])+joinItem i is = case findItem (equalItemType i) is of+ Nothing -> (i, i : is)+ Just (j,js) -> let n = i { icount = icount i + icount j,+ iletter = mergeLetter (iletter j) (iletter i) }+ in (n, n : js)++-- | Removes an item from a list of items. Takes an equality function (i.e., by letter or+-- by type) as an argument.+removeItemBy :: (Item -> Item -> Bool) -> Item -> [Item] -> [Item]+removeItemBy eq i = concatMap $ \ x ->+ if eq i x+ then let remaining = icount x - icount i+ in if remaining > 0+ then [x { icount = remaining }]+ else []+ else [x]++removeItemByLetter = removeItemBy equalItemLetter+removeItemByType = removeItemBy equalItemType++-- | Finds an item in a list of items.+findItem :: (Item -> Bool) -> [Item] -> Maybe (Item, [Item])+findItem p is = findItem' [] is+ where+ findItem' acc [] = Nothing+ findItem' acc (i:is)+ | p i = Just (i, reverse acc ++ is)+ | otherwise = findItem' (i:acc) is++strongestSword :: [Item] -> Int+strongestSword l =+ let aux acc (Item { itype = Sword i }) = max acc i+ aux acc _ = acc+ in foldl aux 0 l++makeObject :: Int -> (String -> String) -> String -> String+makeObject 1 adj obj = let b = adj obj+ in case b of+ (c:_) | c `elem` "aeio" -> "an " ++ b+ _ -> "a " ++ b+makeObject n adj obj = show n ++ " " ++ adj (obj ++ "s")
+ src/ItemState.hs view
@@ -0,0 +1,16 @@+module ItemState where++import Data.Set as S+import Data.Map as M++import Item+import State++identified :: Assocs -> Discoveries -> ItemType -> String -> String+identified a d i+ | i `S.member` d = case i of+ Potion t -> potionType t+ _ -> ("really strange " ++)+ | otherwise = case M.lookup i a of+ Just ap -> appearance ap+ _ -> ("really strange " ++)
+ src/Keybindings.hs view
@@ -0,0 +1,40 @@+module Keybindings where++import Control.Monad+import Control.Monad.State hiding (State)+import Data.Map as M++import Action+import Command+import Display2+import Keys as K++-- | Keybindings.+data Keybindings = Keybindings+ { kdir :: DirCommand,+ kudir :: DirCommand,+ kother :: M.Map K.Key Command+ }++handleKey :: Keybindings -> K.Key -> Action ()+handleKey kb k =+ do+ handleDirection k (caction $ kdir kb) $+ handleUDirection k (caction $ kudir kb) $+ case M.lookup k (kother kb) of+ Just c -> caction c+ Nothing -> abortWith $ "unknown command (" ++ K.showKey k ++ ")"++keyHelp :: Keybindings -> String+keyHelp kb =+ let+ fmt k h = replicate 15 ' ' ++ k ++ replicate ((13 - length k) `max` 1) ' ' +++ h ++ replicate ((30 - length h) `max` 1) ' '+ fmts s = replicate 15 ' ' ++ s ++ replicate ((43 - length s) `max` 1) ' '+ blank = fmt "" ""+ title = fmt "key" "command"+ footer = fmts "(See file PLAYING.markdown.)"+ rest = [ fmt (K.showKey k) h | (k, Described h _) <- M.toAscList (kother kb) ]+ in+ unlines ([blank, title] ++ rest ++ [blank, footer, blank])+
+ src/Keys.hs view
@@ -0,0 +1,32 @@+module Keys where++import Prelude hiding (Left, Right)++-- | Library-independent datatype to represent keys.+data Key =+ Esc+ | Return+ | PgUp+ | PgDn+ | Left+ | Right+ | Up+ | Down+ | End+ | Begin+ | Home+ | Char Char -- ^ a single printable character+ deriving (Ord, Eq)++showKey :: Key -> String+showKey (Char c) = [c]+showKey Esc = "<escape>"+showKey Return = "<return>"+showKey PgUp = "<page-up>"+showKey PgDn = "<page-down>"+showKey Left = "<left>"+showKey Right = "<right>"+showKey Up = "<up>"+showKey Down = "<down>"+showKey End = "<end>"+showKey Home = "<home>"
+ src/LambdaHack.hs view
@@ -0,0 +1,79 @@+module Main where++import System.Directory+import Control.Monad+import Data.Map as M+import Data.Maybe++import Action+import State+import Geometry+import Level+import Dungeon+import Perception+import Display2+import Random+import qualified Save+import Turn+import Item+import qualified Config++main :: IO ()+main = startup start++-- | Either restore a saved game, or setup a new game.+start :: Session -> IO ()+start session =+ do+ -- check if we have a savegame+ config <- Config.config+ f <- Save.file config+ x <- doesFileExist f+ restored <- if x then do+ displayBlankConfirm session "Restoring save game"+ Save.restoreGame config+ else return $ Right "Welcome to LambdaHack!" -- new game+ case restored of+ Right msg -> generate config session msg+ Left state -> handlerToIO session state "Welcome back to LambdaHack."+ handle++-- | Generate the dungeon for a new game, and start the game loop.+generate :: Config.CP -> Session -> String -> IO ()+generate config session msg =+ let matchGenerator n Nothing =+ if n == 3 then bigroom else+ if n == 10 then noiseroom else -- access to stairs may be blocked+ level+ matchGenerator n (Just "bigroom") = bigroom+ matchGenerator n (Just "noiseroom") = noiseroom+ matchGenerator n (Just s) =+ error $ "findGenerator: unknown: " ++ show n ++ ", " ++ s++ findGenerator n =+ let genName = Config.getOption config "dungeon" ("level" ++ show n)+ generator = matchGenerator n genName+ in rndToIO $ generator (defaultLevelConfig n) (LambdaCave n)++ connect :: Maybe (Maybe DungeonLoc) ->+ [(Maybe (Maybe DungeonLoc) -> Maybe (Maybe DungeonLoc) ->+ Level, Loc, Loc)] ->+ [Level]+ connect au [(x,_,_)] = [x au Nothing]+ connect au ((x,_,d):ys@((_,u,_):_)) =+ let (z:zs) = connect (Just (Just (lname x',d))) ys+ x' = x au (Just (Just (lname z,u)))+ in x' : z : zs+ in+ do+ let depth = fromMaybe 10 $ Config.getOption config "dungeon" "depth"+ levels <- mapM findGenerator [1..depth]+ let lvls = connect (Just Nothing) levels+ (lvl,dng) = (head lvls, dungeon (tail lvls))+ -- generate item associations+ assocs = M.fromList+ [ (Potion PotionWater, Clear),+ (Potion PotionHealing, White) ]+ defState = defaultState ((\ (_,x,_) -> x) (head levels)) dng lvl+ state = defState { sassocs = assocs, config = config }+ handlerToIO session state msg handle
+ src/Level.hs view
@@ -0,0 +1,545 @@+module Level where++import qualified System.Random as R+import Control.Monad++import Data.Binary+import Data.Binary.Put+import Data.Binary.Get+import Data.Map as M+import Data.Set as S+import Data.List as L+import Data.Ratio+import Data.Maybe++import Geometry+import Monster+import Item+import Random+import Display++-- | Names of the dungeon levels are represented using a+-- custom data structure.+data LevelName = LambdaCave Int | Exit+ deriving (Show, Eq, Ord)++-- | Chance that a new monster is generated. Currently depends on the+-- number of monsters already present, and on the level. In the future,+-- the strength of the character and the strength of the monsters present+-- could further influence the chance, and the chance could also affect+-- which monster is generated.+monsterGenChance :: LevelName -> [Monster] -> Rnd Bool+monsterGenChance (LambdaCave n) [] = chance $ 1%25+monsterGenChance (LambdaCave n) l = chance $ 1%((400 + (fromIntegral (L.length l) * 100) - (fromIntegral n * 50)) `max` 50)+monsterGenChance _ _ = return False++instance Binary LevelName where+ put (LambdaCave n) = put n+ get = liftM LambdaCave get++-- | Provide a textual description of a level name.+levelName :: LevelName -> String+levelName (LambdaCave n) = "The Lambda Cave " ++ show n++-- | Gives the numeric representation of the level's depth.+levelNumber :: LevelName -> Int+levelNumber (LambdaCave n) = n++-- | The complete dungeon is a map from level names to levels.+-- We usually store all but the current level in this data structure.+data Dungeon = Dungeon (M.Map LevelName Level)+ deriving Show++-- | Create a dungeon from a list of levels.+dungeon :: [Level] -> Dungeon+dungeon = Dungeon . M.fromList . L.map (\ l -> (lname l, l))++-- | Extract a level from a dungeon.+getDungeonLevel :: LevelName -> Dungeon -> (Level, Dungeon)+getDungeonLevel ln (Dungeon dng) = (fromJust (M.lookup ln dng), Dungeon (M.delete ln dng))++-- | Put a level into a dungeon.+putDungeonLevel :: Level -> Dungeon -> Dungeon+putDungeonLevel lvl (Dungeon dng) = Dungeon (M.insert (lname lvl) lvl dng)++instance Binary Dungeon where+ put (Dungeon dng) = put (M.elems dng)+ get = liftM dungeon get++-- | A dungeon location is a level together with a location on+-- that level.+type DungeonLoc = (LevelName, Loc)++data Level = Level+ { lname :: LevelName,+ lsize :: (Y,X),+ lmonsters :: [Monster],+ lsmell :: SMap,+ lmap :: LMap,+ lmeta :: String }+ deriving Show++updateLMap :: (LMap -> LMap) -> Level -> Level+updateLMap f lvl = lvl { lmap = f (lmap lvl) }++updateSMap :: (SMap -> SMap) -> Level -> Level+updateSMap f lvl = lvl { lsmell = f (lsmell lvl) }++updateMonsters :: ([Monster] -> [Monster]) -> Level -> Level+updateMonsters f lvl = lvl { lmonsters = f (lmonsters lvl) }++instance Binary Level where+ put (Level nm sz@(sy,sx) ms lsmell lmap lmeta) =+ do+ put nm+ put sz+ put ms+ put [ lsmell ! (y,x) | y <- [0..sy], x <- [0..sx] ]+ put [ lmap ! (y,x) | y <- [0..sy], x <- [0..sx] ]+ put lmeta+ get = do+ nm <- get+ sz@(sy,sx) <- get+ ms <- get+ xs <- get+ let lsmell = M.fromList (zip [ (y,x) | y <- [0..sy], x <- [0..sx] ] xs)+ xs <- get+ let lmap = M.fromList (zip [ (y,x) | y <- [0..sy], x <- [0..sx] ] xs)+ lmeta <- get+ return (Level nm sz ms lsmell lmap lmeta)++type LMap = Map (Y,X) (Tile,Tile)+type SMap = Map (Y,X) Time++data Tile = Tile+ { tterrain :: Terrain,+ titems :: [Item] }+ deriving Show++instance Binary Tile where+ put (Tile t is) = put t >> put is+ get = liftM2 Tile get get++at l p = fst (findWithDefault (unknown, unknown) p l)+rememberAt l p = snd (findWithDefault (unknown, unknown) p l)++unknown :: Tile+unknown = Tile Unknown []++data Terrain = Rock+ | Opening Pos+ | Floor DL+ | Unknown+ | Corridor+ | Wall Pos+ | Stairs DL VDir (Maybe DungeonLoc)+ | Door Pos (Maybe Int) -- Nothing: open, Just 0: closed, otherwise secret+ deriving Show++instance Binary Terrain where+ put Rock = putWord8 0+ put (Opening p) = putWord8 1 >> put p+ put (Floor dl) = putWord8 2 >> put dl+ put Unknown = putWord8 3+ put Corridor = putWord8 4+ put (Wall p) = putWord8 5 >> put p+ put (Stairs dl d n) = putWord8 6 >> put dl >> put d >> put n+ put (Door p o) = putWord8 7 >> put p >> put o+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Rock+ 1 -> liftM Opening get+ 2 -> liftM Floor get+ 3 -> return Unknown+ 4 -> return Corridor+ 5 -> liftM Wall get+ 6 -> liftM3 Stairs get get get+ 7 -> liftM2 Door get get+ _ -> fail "no parse (Tile)"++data DL = Dark | Light+ deriving (Eq, Show, Bounded)++-- | All the wall types that are possible:+--+-- * 'UL': upper left+--+-- * 'U': upper+--+-- * 'UR': upper right+--+-- * 'L': left+--+-- * 'R': right+--+-- * 'DL': lower left+--+-- * 'D': lower+--+-- * 'DR': lower right+--+-- * 'O': lower right+--+-- I am tempted to add even more (T-pieces and crossings),+-- but currently, we don't need them.+data Pos = UL | U | UR | L | R | DL | D | DR | O+ deriving (Eq, Show, Bounded)++instance Binary Pos where+ put UL = putWord16le 0+ put U = putWord16le 1+ put UR = putWord16le 2+ put L = putWord16le 3+ put R = putWord16le 4+ put DL = putWord16le 5+ put D = putWord16le 6+ put DR = putWord16le 7+ put O = putWord16le 8++ get = do+ tag <- getWord16le+ case tag of+ 0 -> return UL+ 1 -> return U+ 2 -> return UR+ 3 -> return L+ 4 -> return R+ 5 -> return DL+ 6 -> return D+ 7 -> return DR+ 8 -> return O++data HV = Horiz | Vert+ deriving (Eq, Show, Bounded)++fromHV Horiz = True+fromHV Vert = False++toHV True = Horiz+toHV False = Vert++instance R.Random HV where+ randomR (a,b) g = case R.randomR (fromHV a,fromHV b) g of+ (b,g') -> (toHV b,g')+ random g = R.randomR (minBound, maxBound) g++instance Binary HV where+ put Horiz = put True+ put Vert = put False+ get = get >>= \ b -> if b then return Horiz else return Vert++instance Binary DL where+ put Dark = put False+ put Light = put True+ get = get >>= \ b -> if b then return Light else return Dark++data VDir = Up | Down+ deriving (Eq, Show)++instance Binary VDir where+ put Up = put True+ put Down = put False+ get = get >>= \ b -> if b then return Up else return Down++instance Eq Terrain where+ Rock == Rock = True+ Opening d == Opening d' = d == d'+ Floor l == Floor l' = l == l'+ Unknown == Unknown = True+ Corridor == Corridor = True+ Wall p == Wall p' = p == p'+ Stairs dl d t == Stairs dl' d' t' = dl == dl' && d == d' && t == t'+ Door p o == Door p' o' = p == p' && o == o'+ _ == _ = False++-- | blocks moves and vision+closed :: Tile -> Bool+closed = not . open++floor :: Tile -> Bool+floor (Tile { tterrain = Floor _ }) = True+floor _ = False++secret :: Maybe Int -> Bool+secret (Just n) | n /= 0 = True+secret _ = False++toOpen :: Bool -> Maybe Int+toOpen True = Nothing+toOpen False = Just 0++fromDL :: DL -> Bool+fromDL Dark = False+fromDL Light = True++toDL :: Bool -> DL+toDL False = Dark+toDL True = Light++-- | allows moves and vision+open :: Tile -> Bool+open (Tile (Floor {}) _) = True+open (Tile (Opening {}) _) = True+open (Tile (Door _ o) _) = isNothing o+open (Tile Corridor _) = True+open (Tile (Stairs {}) _) = True+open _ = False++-- | is lighted on its own+light :: Tile -> Bool+light (Tile (Floor l) _) = fromDL l+light (Tile (Stairs l _ _) _) = fromDL l+light _ = False++-- | can be lighted by sourrounding tiles+reflects :: Tile -> Bool+reflects (Tile (Opening _) _) = True+reflects (Tile (Wall _) _) = True+reflects (Tile (Door _ _) _) = True+reflects _ = False++-- | Passive tiles reflect light from some other (usually adjacent)+-- positions. This function returns the offsets from which light is+-- reflected. Not all passively lighted tiles reflect from all directions.+-- Walls, for instance, cannot usually be seen from the outside.+passive :: Tile -> [Dir]+passive (Tile (Wall p) _) = posToDir p+passive (Tile (Opening _) _) = moves+passive (Tile (Door p Nothing) _) = moves+passive (Tile (Door p (Just 0)) _) = moves+ -- doors can be seen from all sides+passive (Tile (Door p (Just n)) _) = posToDir p+ -- secret doors are like walls+passive (Tile (Stairs _ _ _) _) = moves+passive _ = []++-- | Perceptible is similar to passive, but describes which tiles can+-- be seen from which adjacent fields in the dark.+perceptible :: Tile -> [Dir]+perceptible (Tile Rock _) = []+perceptible p = case passive p of+ [] -> moves+ ds -> ds++-- | Maps wall types to lists of expected floor positions.+posToDir :: Pos -> [Dir]+posToDir UL = [downright]+posToDir U = [down]+posToDir UR = [downleft]+posToDir L = [right]+posToDir R = [left]+posToDir DL = [upright]+posToDir D = [up]+posToDir DR = [upleft]+posToDir O = moves++-- checks for the presence of monsters (and items); it does *not* check+-- if the tile is open ...+unoccupied :: [Monster] -> LMap -> Loc -> Bool+unoccupied monsters _lmap loc =+ all (\ m -> mloc m /= loc) monsters++-- check whether one location is accessible from the other+-- precondition: the two locations are next to each other+-- currently only implements that doors aren't accessible diagonally,+-- and that the target location has to be open+accessible :: LMap -> Loc -> Loc -> Bool+accessible lmap source target =+ let dir = shift source (neg target)+ src = lmap `at` source+ tgt = lmap `at` target+ in open tgt &&+ (not (diagonal dir) ||+ case (tterrain src, tterrain tgt) of+ (Door {}, _) -> False+ (_, Door {}) -> False+ _ -> True)++-- check whether the location contains a door of at most secrecy level k+openable :: Int -> LMap -> Loc -> Bool+openable k lmap target =+ let tgt = lmap `at` target+ in case tterrain tgt of+ Door _ (Just n) -> n <= k+ _ -> False++findLocInArea :: Area -> (Loc -> Bool) -> Rnd Loc+findLocInArea a@((y0,x0),(y1,x1)) p =+ do+ rx <- randomR (x0,x1)+ ry <- randomR (y0,y1)+ let loc = (ry,rx)+ if p loc then return loc else findLocInArea a p++locInArea :: Area -> Rnd Loc+locInArea a = findLocInArea a (const True)++findLoc :: Level -> (Loc -> Tile -> Bool) -> Rnd Loc+findLoc l@(Level { lsize = sz, lmap = lm }) p =+ do+ loc <- locInArea ((0,0),sz)+ let tile = lm `at` loc+ if p loc tile+ then return loc+ else findLoc l p++findLocTry :: Int -> -- try k times+ Level ->+ (Loc -> Tile -> Bool) -> -- loop until satisfied+ (Loc -> Tile -> Bool) -> -- only try to satisfy k times+ Rnd Loc+findLocTry k l@(Level { lsize = sz, lmap = lm }) p pTry =+ do+ loc <- locInArea ((0,0),sz)+ let tile = lm `at` loc+ if p loc tile && pTry loc tile+ then return loc+ else if k > 1+ then findLocTry (k - 1) l p pTry+ else findLoc l p++grid :: (Y,X) -> Area -> Map (Y,X) Area+grid (ny,nx) ((y0,x0),(y1,x1)) =+ let yd = y1 - y0+ xd = x1 - x0+ in M.fromList [ ((y,x), ((y0 + (yd * y `div` ny), x0 + (xd * x `div` nx)),+ (y0 + (yd * (y + 1) `div` ny - 1), x0 + (xd * (x + 1) `div` nx - 1))))+ | x <- [0..nx-1], y <- [0..ny-1] ]+++connectGrid :: (Y,X) -> Rnd [((Y,X),(Y,X))]+connectGrid (ny,nx) =+ do+ let unconnected = S.fromList [ (y,x) | x <- [0..nx-1], y <- [0..ny-1] ]+ -- candidates are neighbors that are still unconnected; we start with+ -- a random choice+ rx <- randomR (0,nx-1)+ ry <- randomR (0,ny-1)+ let candidates = S.fromList [ (ry,rx) ]+ connectGrid' (ny,nx) unconnected candidates []++randomConnection :: (Y,X) -> Rnd ((Y,X),(Y,X))+randomConnection (ny,nx) =+ do+ rb <- randomR (False,True)+ if rb then do+ rx <- randomR (0,nx-2)+ ry <- randomR (0,ny-1)+ return (normalize ((ry,rx),(ry,rx+1)))+ else do+ ry <- randomR (0,ny-2)+ rx <- randomR (0,nx-1)+ return (normalize ((ry,rx),(ry+1,rx)))++normalize :: ((Y,X),(Y,X)) -> ((Y,X),(Y,X))+normalize (a,b) | a <= b = (a,b)+ | otherwise = (b,a)++normalizeArea :: Area -> Area+normalizeArea a@((y0,x0),(y1,x1)) = ((min y0 y1, min x0 x1), (max y0 y1, max x0 x1))++connectGrid' :: (Y,X) -> Set (Y,X) -> Set (Y,X) -> [((Y,X),(Y,X))] -> Rnd [((Y,X),(Y,X))]+connectGrid' (ny,nx) unconnected candidates acc+ | S.null candidates = return (L.map normalize acc)+ | otherwise = do+ c <- oneOf (S.toList candidates)+ let ns = neighbors ((0,0),(ny-1,nx-1)) c -- potential new candidates+ let nu = S.delete c unconnected -- new unconnected+ let (nc,ds) = S.partition (`S.member` nu) ns+ -- (new candidates, potential connections)+ new <- if S.null ds then return id+ else do+ d <- oneOf (S.toList ds)+ return ((c,d) :)+ connectGrid' (ny,nx) nu+ (S.delete c (candidates `S.union` nc)) (new acc)++neighbors :: Area -> {- size limitation -}+ Loc -> {- location to find neighbors of -}+ Set Loc+neighbors area (y,x) =+ let cs = [ (y + dy, x + dx) | dy <- [-1..1], dx <- [-1..1], (dx + dy) `mod` 2 == 1 ]+ in S.fromList (L.filter (`inside` area) cs)++inside :: Loc -> Area -> Bool+inside (y,x) ((y0,x0),(y1,x1)) = x1 >= x && x >= x0 && y1 >= y && y >= y0+++fromTo :: Loc -> Loc -> [Loc]+fromTo (y0,x0) (y1,x1)+ | y0 == y1 = L.map (\ x -> (y0,x)) (fromTo1 x0 x1)+ | x0 == x1 = L.map (\ y -> (y,x0)) (fromTo1 y0 y1)++fromTo1 :: X -> X -> [X]+fromTo1 x0 x1+ | x0 <= x1 = [x0..x1]+ | otherwise = [x0,x0-1..x1]++-- | Produces a textual description for terrain, used if no objects+-- are present.+lookTerrain :: Terrain -> String+lookTerrain (Floor _) = "Floor."+lookTerrain Corridor = "Corridor."+lookTerrain (Opening _) = "An opening."+lookTerrain (Stairs _ Up _) = "A staircase up."+lookTerrain (Stairs _ Down _) = "A staircase down."+lookTerrain (Door _ Nothing) = "An open door."+lookTerrain _ = ""++-- | The parameter "n" is the level of evolution:+--+-- 0: final+-- 1: stairs added+-- 2: doors added+-- 3: corridors and openings added+-- 4: only rooms+--+-- The Bool indicates whether the loc is currently visible.+viewTerrain :: Int -> Bool -> Terrain -> (Char, Attr -> Attr)+viewTerrain n b Rock = (' ', id)+viewTerrain n b (Opening d)+ | n <= 3 = ('.', id)+ | otherwise = viewTerrain 0 b (Wall d)+viewTerrain n b (Floor Light) = ('.', id)+viewTerrain n b (Floor Dark) = if b then ('.', id) else (' ', id)+viewTerrain n b Unknown = (' ', id)+viewTerrain n b Corridor+ | n <= 3 = ('#', id)+ | otherwise = viewTerrain 0 b Rock+viewTerrain n b (Wall p)+ | p == O = ('O', id)+ | p `elem` [L, R] = ('|', id)+ | otherwise = ('-', id)+viewTerrain n b (Stairs _ Up _)+ | n <= 1 = ('<', id)+ | otherwise = viewTerrain 0 b (Floor Dark)+viewTerrain n b (Stairs _ Down _)+ | n <= 1 = ('>', id)+ | otherwise = viewTerrain 0 b (Floor Dark)+viewTerrain n b (Door d (Just 0))+ | n <= 2 = ('+', setFG yellow)+ | otherwise = viewTerrain n b (Opening d)+viewTerrain n b (Door d (Just _))+ | n <= 2 = viewTerrain n b (Wall d) -- secret door+ | otherwise = viewTerrain n b (Opening d)+viewTerrain n b (Door p Nothing)+ | n <= 2 = (if p `elem` [L, R] then '-' else '|', setFG yellow)+ | otherwise = viewTerrain n b (Opening p)++viewSmell :: Int -> (Char, Attr -> Attr)+viewSmell n = let k | n > 9 = '*'+ | n < 0 = '-'+ | otherwise = head . show $ n+ in (k, setFG black . setBG green)++-- TODO: Really scatter around, if more than one or location occupied.+-- Scatter randomly or not?+-- Perhaps starting in the direction opposite to the player?+scatterItems :: [Item] -> Loc -> Level -> Level+scatterItems items loc lvl@(Level { lmap = lmap }) =+ let joinItems items = foldl (\ acc i -> snd (joinItem i acc)) items+ t = lmap `at` loc+ nt = t { titems = joinItems items (titems t) }+ ntRemember = lmap `rememberAt` loc+ in updateLMap (M.insert loc (nt, ntRemember)) lvl
+ src/LevelState.hs view
@@ -0,0 +1,33 @@+module LevelState where++import Display+import Geometry+import Level+import State+import Item+import ItemState+import Grammar++viewTile :: Bool -> Tile -> Assocs -> (Char, Attr -> Attr)+viewTile b (Tile t []) a = viewTerrain 0 b t+viewTile b (Tile t (i:_)) a = viewItem (itype i) a++-- | Produces a textual description of the items at a location. It's+-- probably correct to use 'at' rather than 'rememberAt' at this point,+-- although we could argue that 'rememberAt' reflects what the player can+-- perceive more correctly ...+--+-- The "detailed" variant is for use with an explicit look command.+lookAt :: Bool -> State -> LMap -> Loc -> String+lookAt detailed s lmap loc+ | detailed = lookTerrain (tterrain (lmap `at` loc)) ++ " " ++ isd+ | otherwise = isd+ where+ is = titems (lmap `at` loc)+ isd = case is of+ [] -> ""+ [i] -> "You see " ++ objectItem s (icount i) (itype i) ++ "."+ [i,j] -> "You see " ++ objectItem s (icount i) (itype i) ++ " and "+ ++ objectItem s (icount j) (itype j) ++ "."+ _ -> "There are several objects here" +++ if detailed then ":" else "."
+ src/Message.hs view
@@ -0,0 +1,30 @@+module Message where++import Data.List as L+import Data.Char++type Message = String++more :: Message+more = " --more-- "++yesno :: Message+yesno = " [yn]"++addMsg :: Message -> Message -> Message+addMsg [] x = x+addMsg xs [] = xs+addMsg xs x = xs ++ " " ++ x++splitMsg :: Int -> Message -> [String]+splitMsg w xs+ | w <= m = [xs] -- border case, we cannot make progress+ | l <= w = [xs] -- no problem, everything fits+ | otherwise = let (pre, post) = splitAt (w - m) xs+ (ppre, ppost) = break (`L.elem` " .,:!;") $ reverse pre+ rpost = dropWhile isSpace ppost+ in if L.null rpost then pre : splitMsg w post+ else reverse rpost : splitMsg w (reverse ppre ++ post)+ where+ m = length more+ l = length xs
+ src/Monster.hs view
@@ -0,0 +1,134 @@+module Monster where++import Data.Char+import Data.Binary+import Control.Monad++import Geometry+import Display+import Item+import Random++-- | Hit points of the player. TODO: Should not be hardcoded.+playerHP :: Int+playerHP = 50++-- | Time the player can be traced by monsters. TODO: Make configurable.+smellTimeout :: Time+smellTimeout = 1000++-- | Initial player.+defaultPlayer :: Loc -> Player+defaultPlayer ploc =+ Monster Player playerHP playerHP Nothing ploc [] 'a' 10 0++type Player = Monster++data Monster = Monster+ { mtype :: !MonsterType,+ mhpmax :: !Int,+ mhp :: !Int,+ mdir :: Maybe Dir, -- for monsters: the dir the monster last moved;+ -- for the player: the dir the player is running+ mloc :: !Loc,+ mitems :: [Item], -- inventory+ mletter :: !Char, -- next inventory letter+ mspeed :: !Time, -- speed (i.e., delay before next action)+ mtime :: !Time } -- time of next action+ deriving Show++instance Binary Monster where+ put (Monster mt mhpm mhp md ml minv mletter mspeed mtime) =+ do+ put mt+ put mhpm+ put mhp+ put md+ put ml+ put minv+ put mletter+ put mspeed+ put mtime+ get = do+ mt <- get+ mhpm <- get+ mhp <- get+ md <- get+ ml <- get+ minv <- get+ mletter <- get+ mspeed <- get+ mtime <- get+ return (Monster mt mhpm mhp md ml minv mletter mspeed mtime)++data MonsterType =+ Player+ | Eye+ | FastEye+ | Nose+ deriving (Show, Eq)++instance Binary MonsterType where+ put Player = putWord8 0+ put Eye = putWord8 1+ put FastEye = putWord8 2+ put Nose = putWord8 3+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Player+ 1 -> return Eye+ 2 -> return FastEye+ 3 -> return Nose+ _ -> fail "no parse (MonsterType)"++-- | Monster frequencies (TODO: should of course vary much more+-- on local circumstances).+monsterFrequency :: Frequency MonsterType+monsterFrequency =+ Frequency+ [+ (2, Nose),+ (6, Eye),+ (1, FastEye)+ ]++-- | Generate monster.+newMonster :: Loc -> Frequency MonsterType -> Rnd Monster+newMonster loc ftp =+ do+ tp <- frequency ftp+ hp <- hps tp+ let s = speed tp+ return (template tp hp loc s)+ where+ -- setting the time of new monsters to 0 makes them able to+ -- move immediately after generation; this does not seem like+ -- a bad idea, but it would certainly be "more correct" to set+ -- the time to the creation time instead+ template tp hp loc s = Monster tp hp hp Nothing loc [] 'a' s 0++ hps Eye = randomR (1,12) -- falls in 1--4 unarmed rounds+ hps FastEye = randomR (1,6) -- 1--2+ hps Nose = randomR (6,13) -- 2--5 and in 1 round of the strongest sword++ speed Eye = 10+ speed FastEye = 4+ speed Nose = 11++-- | Insert a monster in an mtime-sorted list of monsters.+-- Returns the position of the inserted monster and the new list.+insertMonster :: Monster -> [Monster] -> (Int, [Monster])+insertMonster = insertMonster' 0+ where+ insertMonster' n m [] = (n, [m])+ insertMonster' n m (m':ms)+ | mtime m <= mtime m' = (n, m : m' : ms)+ | otherwise = let (n', ms') = insertMonster' (n + 1) m ms+ in (n', m' : ms')++viewMonster :: MonsterType -> (Char, Attr -> Attr)+viewMonster Player = ('@', setBG white . setFG black)+viewMonster Eye = ('e', setFG red)+viewMonster FastEye = ('e', setFG blue)+viewMonster Nose = ('n', setFG green)
+ src/Perception.hs view
@@ -0,0 +1,85 @@+module Perception where++import Data.Set as S+import Data.List as L+import Data.Maybe++import Geometry+import State+import Level+import Monster+import FOV+import qualified Config++data Perception =+ Perception { preachable :: Set Loc, pvisible :: Set Loc }++perception_ :: State -> Perception+perception_ (State { splayer = Monster { mloc = ploc },+ slevel = Level { lmap = lmap},+ config = config,+ ssensory = ssensory }) =+ let mode = Config.getOption config "engine" "fov_mode"+ radius = fromMaybe 40 $ Config.getOption config "engine" "fov_radius"+ fovMode =+ -- terrible, temporary hack+ case ssensory of+ Vision 3 -> Diagonal radius+ Vision 2 -> Permissive radius+ Vision 1 -> Shadow+ _ ->+ -- this is not a hack+ case mode of+ Just "permissive" -> Permissive radius+ Just "diagonal" -> Diagonal radius+ _ -> Shadow+ in perception fovMode ploc lmap++perception :: FovMode -> Loc -> LMap -> Perception+perception fovMode ploc lmap =+ let+ -- This part is simple. "reachable" contains everything that is on an+ -- unblocked path from the player position.+ reachable = fullscan fovMode ploc lmap+ -- In "actVisible", we store the locations that have light and are+ -- reachable. Furthermore, the player location itself is always+ -- visible.+ litVisible = S.filter (\ loc -> light (lmap `at` loc)) reachable+ actVisible = S.insert ploc litVisible+ srnd = S.fromList $ surroundings ploc+ -- In "dirVisible", we store locations in the surroundings that are+ -- perceptible from the current position.+ dirVisible = S.filter (\ loc -> let p = perceptible (lmap `at` loc) :: [Dir]+ in any (\ d -> shift loc d == ploc) p)+ srnd+ ownVisible = S.union actVisible dirVisible+ -- Something is "pasVisible" if it is reachable passively visible from an+ -- "actVisible" location, *or* if it is in the surroundings and passively+ -- visible from a "dirVisible" location. (This is complicated, and I'd+ -- like to simplify it, but for now, it seems to at least do what I+ -- want.)+ pasVisible = S.filter (\ loc -> let p = passive (lmap `at` loc)+ dp = S.member loc srnd+ s = if dp then ownVisible else actVisible+ in any (\ d -> S.member (shift loc d) s) p)+ reachable+ visible = S.unions [pasVisible, actVisible, dirVisible]+ -- A simpler way to make walls of lit rooms visible, at the cost of making+ -- them reflect light from all sides, also from corridors.+ -- Can be hacked around by checking for corridors in the condition below.+ -- The version in the comment assumes player light has diameter 3, not 1,+ -- which looks a bit differently in dark rooms, revealing more walls.+ openSurroundings = S.filter (\ loc -> open (lmap `at` loc)) srnd+ openVisible = S.union actVisible openSurroundings+ simpleVisible =+ S.filter+ (\ loc -> S.member loc openVisible+ || (reflects (lmap `at` loc)+ && L.any+ (\ l -> S.member l actVisible{-openVisible-})+ (surroundings loc))+ ) (S.insert ploc reachable)+ in+ case fovMode of+ Shadow -> Perception reachable visible+ _ -> Perception reachable simpleVisible
+ src/Random.hs view
@@ -0,0 +1,74 @@+module Random (module Frequency, module Random) where++import Data.Ratio+import qualified System.Random as R+import Control.Monad.State++import Frequency++type Rnd a = State R.StdGen a++-- Written in a "portable" way because the implementation of+-- State changes between mtl versions 1 and 2.+randomR :: (R.Random a) => (a, a) -> Rnd a+randomR rng =+ do+ g <- get+ let (x, ng) = R.randomR rng g+ put ng+ return x++binaryChoice :: a -> a -> Rnd a+binaryChoice p0 p1 =+ do+ b <- randomR (False,True)+ return (if b then p0 else p1)++chance :: Rational -> Rnd Bool+chance r =+ do+ let n = numerator r+ d = denominator r+ k <- randomR (1,d)+ return (k <= n)++-- | d for die/dice+d :: Int -> Rnd Int+d x = if x <= 0 then return 0 else randomR (1,x)++oneOf :: [a] -> Rnd a+oneOf xs =+ do+ r <- randomR (0, length xs - 1)+ return (xs !! r)++frequency :: Frequency a -> Rnd a+frequency (Frequency xs) =+ do+ r <- randomR (1, sum (map fst xs))+ return (frequency' r xs)+ where+ frequency' :: Int -> [(Int, a)] -> a+ frequency' _ [(_, x)] = x+ frequency' m ((n, x) : xs)+ | m <= n = x+ | otherwise = frequency' (m - n) xs++rndToIO :: Rnd a -> IO a+rndToIO r =+ do+ g <- R.getStdGen+ let (x,g') = runState r g+ R.setStdGen g'+ return x++-- ** Arithmetic operations on Rnd.++infixl 7 *~+infixl 6 ~+~++(~+~) :: Num a => Rnd a -> Rnd a -> Rnd a+(~+~) = liftM2 (+)++(*~) :: Num a => Int -> Rnd a -> Rnd a+x *~ r = liftM sum (replicateM x r)
+ src/Save.hs view
@@ -0,0 +1,36 @@+module Save where++import System.Directory+import Control.Exception as E hiding (handle)++import File+import Level+import State+import qualified Config++-- | Name of the save game.+file :: Config.CP -> IO String+file config = Config.getFile config "LambdaHack.save" "files" "savegame"++-- | We save a simple serialized version of the current level and+-- the current state. The 'False' is used only as an EOF marker.+saveGame :: State -> IO ()+saveGame state =+ do+ f <- file (config state)+ encodeCompressedFile f (state,False)++-- | Restore a saved game. Returns either the current level and+-- game state, or a string containing an error message if restoring+-- the game fails.+restoreGame :: Config.CP -> IO (Either State String)+restoreGame config =+ E.catch (do+ f <- file config+ r <- strictDecodeCompressedFile f+ removeFile f+ case r of+ (x,z) -> (z :: Bool) `seq` return $ Left x)+ (\ e -> case e :: IOException of+ _ -> return (Right $ "Restore failed: " +++ (unwords . lines) (show e)))
+ src/State.hs view
@@ -0,0 +1,133 @@+module State where++import qualified Data.Map as M+import qualified Data.Set as S+import Control.Monad+import Data.Binary+import qualified Config++import Monster+import Geometry+import Level+import Item+import Message++-- | The 'State' contains all the game state that has to be saved.+-- In practice, we maintain extra state, but that state is state+-- accumulated during a turn or relevant only to the current session.+data State = State+ { splayer :: Player,+ shistory :: [Message],+ ssensory :: SensoryMode,+ sdisplay :: DisplayMode,+ stime :: Time,+ sassocs :: Assocs, -- ^ how does every item appear+ sdiscoveries :: Discoveries, -- ^ items (types) have been discovered+ sdungeon :: Dungeon, -- ^ all but current dungeon level+ slevel :: Level,+ config :: Config.CP+ }+ deriving Show++defaultState :: Loc -> Dungeon -> Level -> State+defaultState ploc dng lvl =+ State+ (defaultPlayer ploc)+ []+ Implicit Normal+ 0+ M.empty+ S.empty+ dng+ lvl+ Config.empty_CP++updatePlayer :: (Monster -> Monster) -> State -> State+updatePlayer f s = s { splayer = f (splayer s) }++updateHistory :: ([String] -> [String]) -> State -> State+updateHistory f s = s { shistory = f (shistory s) }++updateDiscoveries :: (Discoveries -> Discoveries) -> State -> State+updateDiscoveries f s = s { sdiscoveries = f (sdiscoveries s) }++updateLevel :: (Level -> Level) -> State -> State+updateLevel f s = s { slevel = f (slevel s) }++updateTime :: (Time -> Time) -> State -> State+updateTime f s = s { stime = f (stime s) }++toggleVision :: State -> State+toggleVision s = s { ssensory = case ssensory s of Vision 1 -> Implicit; Vision n -> Vision (n-1); _ -> Vision 3 }++toggleSmell :: State -> State+toggleSmell s = s { ssensory = if ssensory s == Smell then Implicit else Smell }++toggleOmniscient :: State -> State+toggleOmniscient s = s { sdisplay = if sdisplay s == Omniscient then Normal else Omniscient }++toggleTerrain :: State -> State+toggleTerrain s = s { sdisplay = case sdisplay s of Terrain 1 -> Normal; Terrain n -> Terrain (n-1); _ -> Terrain 4 }++instance Binary State where+ put (State player hst sense disp time assocs discs dng lvl config) =+ do+ put player+ put hst+ put sense+ put disp+ put time+ put assocs+ put discs+ put dng+ put lvl+ put config+ get =+ do+ player <- get+ hst <- get+ sense <- get+ disp <- get+ time <- get+ assocs <- get+ discs <- get+ dng <- get+ lvl <- get+ config <- get+ return (State player hst sense disp time assocs discs dng lvl config)++data SensoryMode =+ Implicit+ | Vision Int+ | Smell+ deriving (Show, Eq)++instance Binary SensoryMode where+ put Implicit = putWord8 0+ put (Vision n) = putWord8 1 >> put n+ put Smell = putWord8 2+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Implicit+ 1 -> liftM Vision get+ 2 -> return Smell+ _ -> fail "no parse (SensoryMode)"++data DisplayMode =+ Normal+ | Omniscient+ | Terrain Int+ deriving (Show, Eq)++instance Binary DisplayMode where+ put Normal = putWord8 0+ put Omniscient = putWord8 1+ put (Terrain n) = putWord8 2 >> put n+ get = do+ tag <- getWord8+ case tag of+ 0 -> return Normal+ 1 -> return Omniscient+ 2 -> liftM Terrain get+ _ -> fail "no parse (DisplayMode)"
+ src/Strategy.hs view
@@ -0,0 +1,49 @@+module Strategy where++import Control.Monad++import Frequency++-- Monster strategies++-- | A strategy is a choice of frequency tables.+newtype Strategy a = Strategy { runStrategy :: [Frequency a] }+ deriving Show++-- | Strategy is a monad. TODO: Can we write this as a monad transformer?+instance Monad Strategy where+ return x = Strategy $ return (Frequency [(1, x)])+ m >>= f = Strategy $+ filter (\ (Frequency xs) -> not (null xs))+ [ Frequency [ (p * q, b) + | (p, a) <- runFrequency x,+ y <- runStrategy (f a),+ (q, b) <- runFrequency y ] + | x <- runStrategy m ]++liftFrequency :: Frequency a -> Strategy a+liftFrequency f = Strategy [f]++instance MonadPlus Strategy where+ mzero = Strategy []+ mplus (Strategy xs) (Strategy ys) = Strategy (xs ++ ys)++infixr 2 .|++(.|) :: Strategy a -> Strategy a -> Strategy a+(.|) = mplus++reject :: Strategy a+reject = mzero++infix 3 .=>++(.=>) :: Bool -> Strategy a -> Strategy a+p .=> m | p = m+ | otherwise = mzero++only :: (a -> Bool) -> Strategy a -> Strategy a+only p s =+ do+ x <- s+ p x .=> return x
+ src/StrategyState.hs view
@@ -0,0 +1,72 @@+module StrategyState where++import Data.List as L+import Data.Map as M+import Data.Set as S++import Geometry+import Level+import Monster+import Random+import Perception+import Strategy+import State++strategy :: Monster -> State -> Perception -> Strategy Dir+strategy m@(Monster { mtype = mt, mloc = me, mdir = mdir })+ (state@(State { splayer = player@(Monster { mloc = ploc }),+ stime = time,+ slevel = lvl@(Level { lmonsters = ms, lsmell = nsmap, lmap = lmap }) }))+ per =+ case mt of+ Eye -> slowEye+ FastEye -> fastEye+ Nose -> nose+ _ -> onlyAccessible moveRandomly+ where+ -- we check if the monster is visible by the player rather than if the+ -- player is visible by the monster -- this is more efficient, but+ -- won't be correct in the general situation+ playerVisible = me `S.member` pvisible per+ playerAdjacent = adjacent me ploc+ towardsPlayer = towards (me, ploc)+ onlyTowardsPlayer = only (\ x -> distance (towardsPlayer, x) <= 1)+ lootPresent = (\ x -> not $ L.null $ titems $ lmap `at` x)+ onlyLootPresent = onlyMoves lootPresent me+ onlyPreservesDir = only (\ x -> maybe True (\ d -> distance (neg d, x) > 1) mdir)+ onlyUnoccupied = onlyMoves (unoccupied ms lmap) me+ onlyAccessible = onlyMoves (accessible lmap me) me+ onlyOpenable = onlyMoves (openable 10 lmap) me+ smells = L.map fst $+ L.sortBy (\ (_,s1) (_,s2) -> compare s2 s1) $+ L.filter (\ (_,s) -> s > 0) $+ L.map (\ x -> (x, nsmap ! (me `shift` x) - time `max` 0)) moves++ eye = onlyUnoccupied $+ playerVisible .=> onlyTowardsPlayer moveRandomly+ .| lootPresent me .=> return (0,0)+ .| onlyLootPresent moveRandomly+ .| onlyPreservesDir moveRandomly++ slowEye = playerAdjacent .=> return towardsPlayer+ .| not playerVisible .=> onlyOpenable eye+ .| onlyAccessible eye++ fastEye = playerAdjacent .=> return towardsPlayer+ .| onlyAccessible eye++ nose = playerAdjacent .=> return towardsPlayer+ .| (onlyAccessible $+ lootPresent me .=> return (0,0)+ .| foldr (.|) reject (L.map return smells)+ .| onlyLootPresent moveRandomly+ .| moveRandomly)++onlyMoves :: (Dir -> Bool) -> Loc -> Strategy Dir -> Strategy Dir+onlyMoves p l = only (\ x -> p (l `shift` x))++moveRandomly :: Strategy Dir+moveRandomly = liftFrequency $ uniform moves++wait :: Strategy Dir+wait = return (0,0)
+ src/Turn.hs view
@@ -0,0 +1,261 @@+module Turn where++import Control.Monad+import Control.Monad.State hiding (State)+import Data.Map as M++import Action+import Actions+import Actor+import Command+import Display2 hiding (display)+import Keybindings+import qualified Keys as K+import Level+import Monster+import Random+import State+import Strategy+import StrategyState+import Version++-- One turn proceeds through the following functions:+--+-- handle+-- handleMonsters, handleMonster+-- nextMove+-- handle (again)+--+-- OR:+--+-- handle+-- handlePlayer, playerCommand+-- handleMonsters, handleMonster+-- nextMove+-- handle (again)+--+-- What's happening where:+--+-- handle: check for hero's death, HP regeneration, determine who moves next,+-- dispatch to handleMonsters or handlePlayer+--+-- handlePlayer: remember, display, get and process commmand(s),+-- advance player time, update smell map, update perception+--+-- handleMonsters: find monsters that can move or die+--+-- handleMonster: determine and process monster action, advance monster time+--+-- nextMove: advance global game time, monster generation+--+-- This is rather convoluted, and the functions aren't named very aptly, so we+-- should clean this up later. TODO.++-- | Decide if the hero is ready for another move. Dispatch to either 'handleMonsters'+-- or 'handlePlayer'.+handle :: Action ()+handle =+ do+ debug "handle"+ state <- get+ let ptime = mtime (splayer state) -- time of hero's next move+ let time = stime state -- current game time+ checkHeroDeath -- hero can die even if it's not the hero's turn+ regenerate APlayer -- hero can regenerate even if it's not the hero's turn+ debug $ "handle: time check. ptime = " ++ show ptime ++ ", time = " ++ show time+ if ptime > time+ then do+ -- the hero can't make a move yet; monsters first+ -- we redraw the map even between player moves so that the movements of fast+ -- monsters can be traced on the map; we disable this functionality if the+ -- player is currently running, as it would slow down the running process+ -- unnecessarily+ ifRunning (const $ return True) display+ handleMonsters+ else do+ handlePlayer -- it's the hero's turn!++-- | Handle monster moves. Perform moves for individual monsters as long as+-- there are monsters that have a move time which is less than or equal to+-- the current time.+handleMonsters :: Action ()+handleMonsters =+ do+ debug "handleMonsters"+ ms <- gets (lmonsters . slevel)+ time <- gets stime+ case ms of+ [] -> nextMove+ (m@(Monster { mtime = mt }) : ms)+ | mt > time -> -- no monster is ready for another move+ nextMove+ | mhp m <= 0 -> -- the monster dies+ do+ modify (updateLevel (updateMonsters (const ms)))+ -- place the monster's possessions on the map+ modify (updateLevel (scatterItems (mitems m) (mloc m)))+ handleMonsters+ | otherwise -> -- monster m should move; we temporarily remove m from the level+ -- TODO: removal isn't nice. Actor numbers currently change during+ -- a move. This could be cleaned up.+ do+ modify (updateLevel (updateMonsters (const ms)))+ handleMonster m++-- | Handle the move of a single monster.+-- Precondition: monster must not currently be in the monster list of the level.+handleMonster :: Monster -> Action ()+handleMonster m =+ do+ debug "handleMonster"+ state <- get+ let time = stime state+ let ms = lmonsters (slevel state)+ per <- currentPerception+ -- run the AI; it currently returns a direction; TODO: it should return an action+ dir <- liftIO $ rndToIO $ frequency (head (runStrategy (strategy m state per .| wait)))+ let waiting = dir == (0,0)+ let nmdir = if waiting then Nothing else Just dir+ -- advance time and reinsert monster+ let nm = m { mtime = time + mspeed m, mdir = nmdir }+ let (act, nms) = insertMonster nm ms+ modify (updateLevel (updateMonsters (const nms)))+ let actor = AMonster act+ try $ -- if the following action aborts, we just continue+ if waiting+ then+ -- monster is not moving, let's try to pick up an object+ actorPickupItem actor+ else+ moveOrAttack True True actor dir+ handleMonsters++-- | After everything has been handled for the current game time, we can+-- advance the time. Here is the place to do whatever has to be done for+-- every time unit; currently, that's monster generation.+-- TODO: nextMove may not be a good name. It's part of the problem of the+-- current design that all of the top-level functions directly call each+-- other, rather than being called by a driver function.+nextMove :: Action ()+nextMove =+ do+ debug "nextMove"+ modify (updateTime (+1))+ generateMonster+ handle++-- | Handle the move of the hero.+handlePlayer :: Action ()+handlePlayer =+ do+ debug "handlePlayer"+ remember -- the hero perceives his (potentially new) surroundings+ playerCommand -- get and process a player command+ -- at this point, the command was successful+ advanceTime APlayer -- TODO: the command handlers should advance the move time+ state <- get+ let time = stime state+ let loc = mloc (splayer state)+ -- update smell+ modify (updateLevel (updateSMap (M.insert loc (time + smellTimeout))))+ -- determine player perception and continue with monster moves+ withPerception handleMonsters++-- | Determine and process the next player command.+playerCommand :: Action ()+playerCommand =+ do+ display -- draw the current surroundings+ history -- update the message history and reset current message+ tryRepeatedlyWith stopRunning $ do -- on abort, just ask for a new command+ ifRunning continueRun $ do+ k <- session nextCommand+ handleKey stdKeybindings k++ -- Design thoughts (in order to get rid or partially rid of the somewhat+ -- convoluted design we have): We have three kinds of commands.+ --+ -- Normal commands: they take time, so after handling the command, state changes,+ -- time passes and monsters get to move.+ --+ -- Instant commands: they take no time, and do not change the state.+ --+ -- Meta commands: they take no time, but may change the state.+ --+ -- Ideally, they can all be handled via the same (event) interface. We maintain an+ -- event queue where we store what has to be handled next. The event queue is a sorted+ -- list where every event contains the timestamp when the event occurs. The current game+ -- time is equal to the head element of the event queue. Currently, we only have action+ -- events. An actor gets to move on an event. The actor is responsible for reinsterting+ -- itself in the event queue. Possible new events may include HP regeneration events,+ -- monster generation events, or actor death events.+ --+ -- If an action does not take any time, the actor just reinserts itself with the current+ -- time into the event queue. If the insert algorithm makes sure that later events with+ -- the same time get precedence, this will work just fine.+ --+ -- It's important that we decouple issues like HP regeneration from action events if we+ -- do it like that, because otherwise, HP regeneration may occur multiple times.+ --+ -- Given this scheme, we may get orphaned events: a HP regeneration event for a dead+ -- monster may be scheduled. Or a move event for a monster suddenly put to sleep. We+ -- therefore have to given handlers the option of accessing and cleaning up the event+ -- queue.++-- The remaining functions in this module are individual actions or helper+-- functions.++-- TODO: Should be defined in Command module.+helpCommand = Described "display help" displayHelp++-- | Display command help. TODO: Should be defined in Actions module.+displayHelp :: Action ()+displayHelp = messageOverlayConfirm "Basic keys:" helpString >> abort+ where+ helpString = keyHelp stdKeybindings++stdKeybindings :: Keybindings+stdKeybindings = Keybindings+ { kdir = moveDirCommand,+ kudir = runDirCommand,+ kother = M.fromList $+ [ -- interaction with the dungeon+ (K.Char 'o', openCommand),+ (K.Char 'c', closeCommand),+ (K.Char 's', searchCommand),++ (K.Char '<', ascendCommand),+ (K.Char '>', descendCommand),++ (K.Char ':', lookCommand),++ -- items+ (K.Char ',', pickupCommand),+ (K.Char 'd', dropCommand),+ (K.Char 'i', inventoryCommand),+ (K.Char 'q', drinkCommand),++ -- wait+ -- (K.Char ' ', waitCommand),+ (K.Char '.', waitCommand),++ -- saving or ending the game+ (K.Char 'S', saveCommand),+ (K.Char 'Q', quitCommand),+ (K.Esc , Undescribed $ abortWith "Press Q to quit."),++ -- debug modes+ (K.Char 'V', Undescribed $ modify toggleVision >> withPerception playerCommand),+ (K.Char 'R', Undescribed $ modify toggleSmell >> playerCommand),+ (K.Char 'O', Undescribed $ modify toggleOmniscient >> playerCommand),+ (K.Char 'T', Undescribed $ modify toggleTerrain >> playerCommand),+ (K.Char 'I', Undescribed $ gets (lmeta . slevel) >>= abortWith),++ -- information for the player+ (K.Char 'v', Undescribed $ abortWith version),+ (K.Char 'M', historyCommand),+ (K.Char '?', helpCommand),+ (K.Return , helpCommand)+ ]+ }+
+ src/Version.hs view
@@ -0,0 +1,12 @@+module Version where++import Data.Version++-- Cabal+import qualified Paths_LambdaHack as Self (version)++import Display++version :: String+version = showVersion Self.version ++ " (" ++ displayId ++ " frontend)"+