module Game.LambdaHack.Common.Vector
( Vector(..), isUnit, isDiagonal, neg, chessDistVector, euclidDistSqVector
, moves, compassText, vicinity, vicinityCardinal
, shift, shiftBounded, trajectoryToPath, displacement, pathToTrajectory
, RadianAngle, rotate, towards
, BfsDistance, MoveLegal(..), apartBfs
, fillBfs, findPathBfs, accessBfs, posAimsPos
) where
import Control.Arrow (second)
import Control.Exception.Assert.Sugar
import Data.Binary
import Data.Bits (Bits, complement, (.&.), (.|.))
import qualified Data.EnumMap.Strict as EM
import Data.Int (Int32)
import Data.List
import Data.Maybe
import qualified Data.Sequence as Seq
import Data.Text (Text)
import Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
data Vector = Vector
{ vx :: !X
, vy :: !Y
}
deriving (Eq, Ord, Show, Read)
instance Binary Vector where
put = put . (fromIntegral :: Int -> Int32) . fromEnum
get = fmap (toEnum . (fromIntegral :: Int32 -> Int)) get
instance Enum Vector where
fromEnum = fromEnumVector
toEnum = toEnumVector
maxVectorDim :: Int
maxVectorDim = 2 ^ (maxLevelDimExponent 1) 1
fromEnumVector :: Vector -> Int
fromEnumVector (Vector vx vy) = vx + vy * (2 ^ maxLevelDimExponent)
toEnumVector :: Int -> Vector
toEnumVector n =
let (y, x) = n `quotRem` (2 ^ maxLevelDimExponent)
(vx, vy) = if x > maxVectorDim
then (x 2 ^ maxLevelDimExponent, y + 1)
else if x < maxVectorDim
then (x + 2 ^ maxLevelDimExponent, y 1)
else (x, y)
in Vector{..}
isUnit :: Vector -> Bool
isUnit v = chessDistVector v == 1
isDiagonal :: Vector -> Bool
isDiagonal (Vector x y) = x * y /= 0
neg :: Vector -> Vector
neg (Vector vx vy) = Vector (vx) (vy)
euclidDistSqVector :: Vector -> Vector -> Int
euclidDistSqVector (Vector x0 y0) (Vector x1 y1) =
let square n = n ^ (2 :: Int)
in square (x1 x0) + square (y1 y0)
chessDistVector :: Vector -> Int
chessDistVector (Vector x y) = max (abs x) (abs y)
moves :: [Vector]
moves =
map (uncurry Vector)
[(1, 1), (0, 1), (1, 1), (1, 0), (1, 1), (0, 1), (1, 1), (1, 0)]
moveTexts :: [Text]
moveTexts = ["NW", "N", "NE", "E", "SE", "S", "SW", "W"]
compassText :: Vector -> Text
compassText v = let m = EM.fromList $ zip moves moveTexts
in fromMaybe (assert `failure` "not a unit vector"
`twith` v) $ EM.lookup v m
movesCardinal :: [Vector]
movesCardinal = map (uncurry Vector) [(0, 1), (1, 0), (0, 1), (1, 0)]
vicinity :: X -> Y
-> Point
-> [Point]
vicinity lxsize lysize p =
[ res | dxy <- moves
, let res = shift p dxy
, inside res (0, 0, lxsize 1, lysize 1) ]
vicinityCardinal :: X -> Y
-> Point
-> [Point]
vicinityCardinal lxsize lysize p =
[ res | dxy <- movesCardinal
, let res = shift p dxy
, inside res (0, 0, lxsize 1, lysize 1) ]
shift :: Point -> Vector -> Point
shift (Point x0 y0) (Vector x1 y1) = Point (x0 + x1) (y0 + y1)
shiftBounded :: X -> Y -> Point -> Vector -> Point
shiftBounded lxsize lysize pos v@(Vector xv yv) =
if inside pos (xv, yv, lxsize xv 1, lysize yv 1)
then shift pos v
else pos
trajectoryToPath :: Point -> [Vector] -> [Point]
trajectoryToPath _ [] = []
trajectoryToPath start (v : vs) = let next = shift start v
in next : trajectoryToPath next vs
displacement :: Point -> Point -> Vector
displacement (Point x0 y0) (Point x1 y1) = Vector (x1 x0) (y1 y0)
pathToTrajectory :: [Point] -> [Vector]
pathToTrajectory [] = []
pathToTrajectory lp1@(_ : lp2) = zipWith displacement lp1 lp2
type RadianAngle = Double
rotate :: RadianAngle -> Vector -> Vector
rotate angle (Vector x' y') =
let x = fromIntegral x'
y = fromIntegral y'
dx = x * cos (angle) y * sin (angle)
dy = x * sin (angle) + y * cos (angle)
in normalize dx dy
normalize :: Double -> Double -> Vector
normalize dx dy =
assert (dx /= 0 || dy /= 0 `blame` "can't normalize zero" `twith` (dx, dy)) $
let angle :: Double
angle = atan (dy / dx) / (pi / 2)
dxy | angle <= 0.75 && angle >= 1.25 = (0, 1)
| angle <= 0.25 = (1, 1)
| angle <= 0.25 = (1, 0)
| angle <= 0.75 = (1, 1)
| angle <= 1.25 = (0, 1)
| otherwise = assert `failure` "impossible angle"
`twith` (dx, dy, angle)
in if dx >= 0
then uncurry Vector dxy
else neg $ uncurry Vector dxy
normalizeVector :: Vector -> Vector
normalizeVector v@(Vector vx vy) =
let res = normalize (fromIntegral vx) (fromIntegral vy)
in assert (not (isUnit v) || v == res
`blame` "unit vector gets untrivially normalized"
`twith` (v, res))
res
towards :: Point -> Point -> Vector
towards pos0 pos1 =
assert (pos0 /= pos1 `blame` "towards self" `twith` (pos0, pos1))
$ normalizeVector $ displacement pos0 pos1
newtype BfsDistance = BfsDistance Word8
deriving (Show, Eq, Ord, Enum, Bounded, Bits)
data MoveLegal = MoveBlocked | MoveToOpen | MoveToUnknown
deriving Eq
minKnownBfs :: BfsDistance
minKnownBfs = toEnum $ (1 + fromEnum (maxBound :: BfsDistance)) `div` 2
apartBfs :: BfsDistance
apartBfs = pred minKnownBfs
fillBfs :: (Point -> Point -> MoveLegal)
-> (Point -> Point -> Bool)
-> Point
-> PointArray.Array BfsDistance
-> PointArray.Array BfsDistance
fillBfs isEnterable passUnknown origin aInitial =
let maxUnknownBfs = pred apartBfs
maxKnownBfs = pred maxBound
bfs :: Seq.Seq (Point, BfsDistance)
-> PointArray.Array BfsDistance
-> PointArray.Array BfsDistance
bfs q a =
case Seq.viewr q of
Seq.EmptyR -> a
_ Seq.:> (_, d)
| d == maxUnknownBfs || d == maxKnownBfs -> a
q1 Seq.:> (pos, oldDistance) | oldDistance >= minKnownBfs ->
let distance = succ oldDistance
allMvs = map (shift pos) moves
freshMv p = a PointArray.! p == apartBfs
freshMvs = filter freshMv allMvs
legal p = (p, isEnterable pos p)
legalities = map legal freshMvs
notBlocked = filter ((/= MoveBlocked) . snd) legalities
legalToDist l = if l == MoveToOpen
then distance
else distance .&. complement minKnownBfs
mvs = map (second legalToDist) notBlocked
q2 = foldr (Seq.<|) q1 mvs
s2 = a PointArray.// mvs
in bfs q2 s2
q1 Seq.:> (pos, oldDistance) ->
let distance = succ oldDistance
allMvs = map (shift pos) moves
goodMv p = a PointArray.! p == apartBfs && passUnknown pos p
mvs = zip (filter goodMv allMvs) (repeat distance)
q2 = foldr (Seq.<|) q1 mvs
s2 = a PointArray.// mvs
in bfs q2 s2
origin0 = (origin, minKnownBfs)
in bfs (Seq.singleton origin0) (aInitial PointArray.// [origin0])
findPathBfs :: (Point -> Point -> MoveLegal)
-> (Point -> Point -> Bool)
-> Point -> Point -> Int -> PointArray.Array BfsDistance
-> Maybe [Point]
findPathBfs isEnterable passUnknown source target sepsRaw bfs =
assert (bfs PointArray.! source == minKnownBfs) $
let targetDist = bfs PointArray.! target
in if targetDist == apartBfs
then Nothing
else
let eps = abs sepsRaw `mod` length moves
mix (x : xs) ys = x : mix ys xs
mix [] ys = ys
preferedMoves = let (ch1, ch2) = splitAt eps moves
ch = ch2 ++ ch1
in mix ch (reverse ch)
track :: Point -> BfsDistance -> [Point] -> [Point]
track pos oldDist suffix | oldDist == minKnownBfs =
assert (pos == source
`blame` (source, target, pos, suffix)) suffix
track pos oldDist suffix | oldDist > minKnownBfs =
let dist = pred oldDist
children = map (shift pos) preferedMoves
matchesDist p = bfs PointArray.! p == dist
&& isEnterable p pos == MoveToOpen
minP = fromMaybe (assert `failure` (pos, oldDist, children))
(find matchesDist children)
in track minP dist (pos : suffix)
track pos oldDist suffix =
let distUnknown = pred oldDist
distKnown = distUnknown .|. minKnownBfs
children = map (shift pos) preferedMoves
matchesDistUnknown p = bfs PointArray.! p == distUnknown
&& passUnknown p pos
matchesDistKnown p = bfs PointArray.! p == distKnown
&& isEnterable p pos == MoveToUnknown
(minP, dist) = case find matchesDistKnown children of
Just p -> (p, distKnown)
Nothing -> case find matchesDistUnknown children of
Just p -> (p, distUnknown)
Nothing -> assert `failure` (pos, oldDist, children)
in track minP dist (pos : suffix)
in Just $ track target targetDist []
accessBfs :: PointArray.Array BfsDistance -> Point -> Maybe Int
accessBfs bfs target =
let dist = bfs PointArray.! target
in if dist == apartBfs
then Nothing
else Just $ fromEnum $ dist .&. complement minKnownBfs
posAimsPos :: PointArray.Array BfsDistance -> Point -> Point -> Bool
posAimsPos bfs bpos target =
let mdist = accessBfs bfs target
in mdist == Just (chessDist bpos target)