File: Point.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,056 kB
  • sloc: haskell: 45,636; makefile: 219
file content (194 lines) | stat: -rw-r--r-- 7,557 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
{-# LANGUAGE DeriveGeneric #-}
-- | Basic operations on 2D points represented as linear offsets.
module Game.LambdaHack.Common.Point
  ( Point(..), PointI
  , chessDist, euclidDistSq, adjacent, bresenhamsLineAlgorithm, fromTo
  , originPoint, insideP
  , speedupHackXSize
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , bresenhamsLineAlgorithmBegin, balancedWord
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Data.Binary
import           Data.Int (Int32)
import qualified Data.Primitive.PrimArray as PA
import           GHC.Generics (Generic)
import           Test.QuickCheck

import Game.LambdaHack.Definition.Defs

-- | This is a hack to pass the X size of the dungeon, defined
-- in game content, to the @Enum@ instances of @Point@ and @Vector@.
-- This is already slower and has higher allocation than
-- hardcoding the value, so passing the value explicitly to
-- a generalization of the @Enum@ conversions is out of the question.
-- Perhaps this can be done cleanly and efficiently at link-time
-- via Backpack, but it's probably not supported yet by GHCJS (not verified).
-- For now, we need to be careful never to modify this array,
-- except for setting it at program start before it's used for the first time.
-- Which is easy, because @Point@ is never mentioned in content definitions.
-- The @PrimArray@ has much smaller overhead than @IORef@
-- and reading from it looks cleaner, hence its use.
speedupHackXSize :: PA.PrimArray X
{-# NOINLINE speedupHackXSize #-}
speedupHackXSize = PA.primArrayFromList [80]  -- updated at program startup

-- | 2D points in cartesian representation. Coordinates grow to the right
-- and down, so that the (0, 0) point is in the top-left corner
-- of the screen. Coordinates are never negative
-- (unlike for 'Game.LambdaHack.Common.Vector.Vector')
-- and the @X@ coordinate never reaches the screen width as read
-- from 'speedupHackXSize'.
data Point = Point
  { px :: X
  , py :: Y
  }
  deriving (Eq, Ord, Generic)

instance Show Point where
  show (Point x y) = show (x, y)

instance Binary Point where
  put = put . (toIntegralCrash :: Int -> Int32) . fromEnum
  get = fmap (toEnum . (fromIntegralWrap :: Int32 -> Int)) get
    -- `fromIntegralWrap` is fine here, because we converted the integer
    -- in the opposite direction first, so it fits even in 31 bit `Int`

-- Note that @Ord@ on @Int@ is not monotonic wrt @Ord@ on @Point@.
-- We need to keep it that way, because we want close xs to have close indexes,
-- e.g., adjacent points in line to have adjacent enumerations,
-- because some of the screen layout and most of processing is line-by-line.
-- Consequently, one can use EM.fromDistinctAscList
-- on @(1, 8)..(10, 8)@, but not on @(1, 7)..(10, 9)@.
instance Enum Point where
  fromEnum Point{..} =
    let !xsize = PA.indexPrimArray speedupHackXSize 0
    in
#ifdef WITH_EXPENSIVE_ASSERTIONS
       assert (px >= 0 && py >= 0 && px < xsize
              `blame` "invalid point coordinates"
              `swith` (px, py))
#endif
         (px + py * xsize)
  toEnum n = let !xsize = PA.indexPrimArray speedupHackXSize 0
                 (py, px) = n `quotRem` xsize
             in Point{..}

instance Arbitrary Point where
  arbitrary = do
    let xsize = PA.indexPrimArray speedupHackXSize 0
    n <- getSize
    Point <$> choose (0, min n (xsize - 1))
          <*> choose (0, n)

-- | Enumeration representation of @Point@.
type PointI = Int

-- This is hidden from Haddock, but run by doctest:
-- $
-- prop> (toEnum :: PointI -> Point) (fromEnum p) == p
-- prop> \ (NonNegative i) -> (fromEnum :: Point -> PointI) (toEnum i) == i

-- | The distance between two points in the chessboard metric.
--
-- >>> chessDist (Point 0 0) (Point 0 0)
-- 0
-- >>> chessDist (Point (-1) 0) (Point 0 0)
-- 1
-- >>> chessDist (Point (-1) 0) (Point (-1) 1)
-- 1
-- >>> chessDist (Point (-1) 0) (Point 0 1)
-- 1
-- >>> chessDist (Point (-1) 0) (Point 1 1)
-- 2
--
-- prop> chessDist p1 p2 >= 0
-- prop> chessDist p1 p2 ^ (2 :: Int) <= euclidDistSq p1 p2
chessDist :: Point -> Point -> Int
chessDist (Point x0 y0) (Point x1 y1) = max (abs (x1 - x0)) (abs (y1 - y0))

-- | Squared euclidean distance between two points.
euclidDistSq :: Point -> Point -> Int
euclidDistSq (Point x0 y0) (Point x1 y1) =
  (x1 - x0) ^ (2 :: Int) + (y1 - y0) ^ (2 :: Int)

-- | Checks whether two points are adjacent on the map
-- (horizontally, vertically or diagonally).
adjacent :: Point -> Point -> Bool
{-# INLINE adjacent #-}
adjacent s t = chessDist s t == 1

-- | Bresenham's line algorithm generalized to arbitrary starting @eps@
-- (@eps@ value of 0 gives the standard BLA).
-- Skips the source point and goes through the second point to infinity.
-- Gives @Nothing@ if the points are equal. The target is given as @Point@,
-- not @PointI@, to permit aiming out of the level, e.g., to get
-- uniform distributions of directions for explosions close to the edge
-- of the level.
--
-- >>> bresenhamsLineAlgorithm 0 (Point 0 0) (Point 0 0)
-- Nothing
-- >>> take 3 $ fromJust $ bresenhamsLineAlgorithm 0 (Point 0 0) (Point 1 0)
-- [(1,0),(2,0),(3,0)]
-- >>> take 3 $ fromJust $ bresenhamsLineAlgorithm 0 (Point 0 0) (Point 0 1)
-- [(0,1),(0,2),(0,3)]
-- >>> take 3 $ fromJust $ bresenhamsLineAlgorithm 0 (Point 0 0) (Point 1 1)
-- [(1,1),(2,2),(3,3)]
bresenhamsLineAlgorithm :: Int -> Point -> Point -> Maybe [Point]
bresenhamsLineAlgorithm eps source target =
  if source == target then Nothing
  else Just $ tail $ bresenhamsLineAlgorithmBegin eps source target

-- | Bresenham's line algorithm generalized to arbitrary starting @eps@
-- (@eps@ value of 0 gives the standard BLA). Includes the source point
-- and goes through the target point to infinity.
--
-- >>> take 4 $ bresenhamsLineAlgorithmBegin 0 (Point 0 0) (Point 2 0)
-- [(0,0),(1,0),(2,0),(3,0)]
bresenhamsLineAlgorithmBegin :: Int -> Point -> Point -> [Point]
bresenhamsLineAlgorithmBegin eps (Point x0 y0) (Point x1 y1) =
  let (dx, dy) = (x1 - x0, y1 - y0)
      xyStep b (x, y) = (x + signum dx,     y + signum dy * b)
      yxStep b (x, y) = (x + signum dx * b, y + signum dy)
      (p, q, step) | abs dx > abs dy = (abs dy, abs dx, xyStep)
                   | otherwise       = (abs dx, abs dy, yxStep)
      bw = balancedWord p q (eps `mod` max 1 q)
      walk w xy = xy : walk (tail w) (step (head w) xy)
  in map (uncurry Point) $ walk bw (x0, y0)

-- | See <http://roguebasin.roguelikedevelopment.org/index.php/index.php?title=Digital_lines>.
balancedWord :: Int -> Int -> Int -> [Int]
balancedWord p q eps | eps + p < q = 0 : balancedWord p q (eps + p)
balancedWord p q eps               = 1 : balancedWord p q (eps + p - q)

-- | A list of all points on a straight vertical or straight horizontal line
-- between two points. Fails if no such line exists.
--
-- >>> fromTo (Point 0 0) (Point 2 0)
-- [(0,0),(1,0),(2,0)]
fromTo :: Point -> Point -> [Point]
fromTo (Point x0 y0) (Point x1 y1) =
 let fromTo1 :: Int -> Int -> [Int]
     fromTo1 z0 z1
       | z0 <= z1  = [z0..z1]
       | otherwise = [z0,z0-1..z1]
     result
       | x0 == x1 = map (Point x0) (fromTo1 y0 y1)
       | y0 == y1 = map (`Point` y0) (fromTo1 x0 x1)
       | otherwise = error $ "diagonal fromTo"
                             `showFailure` ((x0, y0), (x1, y1))
 in result

originPoint :: Point
originPoint = Point 0 0

-- | Checks that a point belongs to an area.
insideP :: (X, Y, X, Y) -> Point -> Bool
{-# INLINE insideP #-}
insideP (x0, y0, x1, y1) (Point x y) = x1 >= x && x >= x0 && y1 >= y && y >= y0