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
|