moonlight-planar-1.1.0.0: src-hex/Moonlight/Hex/Region/Internal.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
-- | Trusted packed representation shared by the public region algebra and its
-- topology interpretation. This module is private to the @hex@ component.
module Moonlight.Hex.Region.Internal
( HexLayout (..)
, HexRegion (..)
, extractBits
, finalWordMask
, firstSameLayoutDifferenceIndex
, generateHexRegionIndexed
, generateHexRegionIndexedM
, hexCoordAtIndex
, hexCoordIndex
, hexRegionMemberAtIndex
, lowBitMask
, setBitAt
) where
import Control.DeepSeq (NFData)
import Data.Bits ((.&.), (.|.), complement, countTrailingZeros, shiftL, shiftR)
import Data.Vector.Unboxed qualified as U
import Data.Word (Word64)
import GHC.Generics (Generic)
import Moonlight.Hex.Coordinate (HexCoord (..))
-- | A nonempty axial parallelogram with an overflow-safe dense row-major index.
data HexLayout = HexLayout
{ layoutOrigin :: !HexCoord
, layoutWidth :: !Int
, layoutHeight :: !Int
, layoutCellCount :: !Int
, layoutWordCount :: !Int
}
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Canonical packed membership: one bit per layout cell and zero padding.
data HexRegion = HexRegion !HexLayout !(U.Vector Word64)
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | Construct canonical packed membership from a dense row-major predicate.
-- The index is private representation vocabulary; public callers author by
-- coordinate through 'Moonlight.Hex.Region.hexRegionGenerateM'.
generateHexRegionIndexedM
:: Monad effect
=> HexLayout
-> (Int -> effect Bool)
-> effect HexRegion
generateHexRegionIndexedM layout predicate =
HexRegion layout <$> U.generateM (layoutWordCount layout) generateWord
where
generateWord wordIndex =
let baseIndex = wordIndex * 64
bitCount = min 64 (layoutCellCount layout - baseIndex)
in gatherBits baseIndex bitCount 0 0
gatherBits !baseIndex !bitCount !bitIndex !word
| bitIndex >= bitCount = pure word
| otherwise = do
selected <- predicate (baseIndex + bitIndex)
gatherBits
baseIndex
bitCount
(bitIndex + 1)
(if selected then setBitAt word bitIndex else word)
{-# INLINE generateHexRegionIndexedM #-}
generateHexRegionIndexed :: HexLayout -> (Int -> Bool) -> HexRegion
generateHexRegionIndexed layout predicate =
HexRegion layout (U.generate (layoutWordCount layout) generateWord)
where
generateWord wordIndex =
let baseIndex = wordIndex * 64
bitCount = min 64 (layoutCellCount layout - baseIndex)
in gatherBits baseIndex bitCount 0 0
gatherBits !baseIndex !bitCount !bitIndex !word
| bitIndex >= bitCount = word
| predicate (baseIndex + bitIndex) =
gatherBits baseIndex bitCount (bitIndex + 1) (setBitAt word bitIndex)
| otherwise = gatherBits baseIndex bitCount (bitIndex + 1) word
{-# INLINE generateHexRegionIndexed #-}
-- | Membership at an index already proved to belong to the region's layout.
hexRegionMemberAtIndex :: HexRegion -> Int -> Bool
hexRegionMemberAtIndex (HexRegion _ wordsValue) index =
let (wordIndex, bitIndex) = index `quotRem` 64
in wordsValue `U.unsafeIndex` wordIndex .&. (1 `shiftL` bitIndex) /= 0
{-# INLINE hexRegionMemberAtIndex #-}
-- | First selected cell in the left region absent from the right region.
-- Callers establish equal layouts, which entails equal canonical word counts.
firstSameLayoutDifferenceIndex :: HexRegion -> HexRegion -> Maybe Int
firstSameLayoutDifferenceIndex (HexRegion _ left) (HexRegion _ right) = descend 0
where
descend wordIndex
| wordIndex >= U.length left = Nothing
| otherwise =
let difference =
left `U.unsafeIndex` wordIndex
.&. complement (right `U.unsafeIndex` wordIndex)
in if difference == 0
then descend (wordIndex + 1)
else Just (wordIndex * 64 + countTrailingZeros difference)
{-# INLINE firstSameLayoutDifferenceIndex #-}
extractBits :: U.Vector Word64 -> Int -> Int -> Word64
extractBits wordsValue bitIndex count
| count <= 0 = 0
| otherwise =
let (wordIndex, bitOffset) = bitIndex `quotRem` 64
low = wordsValue `U.unsafeIndex` wordIndex `shiftR` bitOffset
high =
if bitOffset == 0 || bitOffset + count <= 64
then 0
else wordsValue `U.unsafeIndex` (wordIndex + 1) `shiftL` (64 - bitOffset)
in (low .|. high) .&. lowBitMask count
{-# INLINE extractBits #-}
hexCoordIndex :: HexLayout -> HexCoord -> Maybe Int
hexCoordIndex layout (HexCoord q r)
| q < originQ || r < originR = Nothing
| q > maximumQ || r > maximumR = Nothing
| otherwise = Just ((r - originR) * layoutWidth layout + (q - originQ))
where
HexCoord originQ originR = layoutOrigin layout
maximumQ = originQ + layoutWidth layout - 1
maximumR = originR + layoutHeight layout - 1
{-# INLINE hexCoordIndex #-}
hexCoordAtIndex :: HexLayout -> Int -> HexCoord
hexCoordAtIndex layout index =
let (row, column) = index `quotRem` layoutWidth layout
HexCoord originQ originR = layoutOrigin layout
in HexCoord (originQ + column) (originR + row)
{-# INLINE hexCoordAtIndex #-}
finalWordMask :: HexLayout -> Word64
finalWordMask layout =
lowBitMask (((layoutCellCount layout - 1) `rem` 64) + 1)
{-# INLINE finalWordMask #-}
lowBitMask :: Int -> Word64
lowBitMask count
| count >= 64 = maxBound
| count <= 0 = 0
| otherwise = (1 `shiftL` count) - 1
{-# INLINE lowBitMask #-}
setBitAt :: Word64 -> Int -> Word64
setBitAt word bitIndex = word .|. (1 `shiftL` bitIndex)
{-# INLINE setBitAt #-}