packages feed

hexmino-0.1.1.0: src/TileGrid.hs

module TileGrid where

import qualified Tile
import qualified Hex
import qualified Data.Map.Strict as Map
import Data.List (foldl', sortOn)
import qualified System.Random as Rand
import qualified Graphics.Gloss.Data.Color as Color
import qualified Graphics.Gloss.Data.Picture as Pict
import qualified Math.Geometry.Grid as Grid
import qualified Math.Geometry.Grid.Hexagonal2 as HexGrid
import qualified Math.Geometry.GridMap as GridMap (toGrid, toMap, lookup, insert, delete, toList, adjust, elems)
import qualified Math.Geometry.GridMap.Lazy as LGridMap
import Math.Geometry.Grid.HexagonalInternal2 (HexDirection(..))

-- implementation was guided by https://www.redblobgames.com/grids/hexagons/
-- uses grid package, for reference: flat top with Axial (x,y) coordinates
type TileGrid = LGridMap.LGridMap HexGrid.HexHexGrid Tile.Tile
type Index = Grid.Index HexGrid.HexHexGrid

-- creation functions
empty :: TileGrid
empty = LGridMap.empty $ HexGrid.hexHexGrid 0

newGame :: Int -> Rand.StdGen -> (TileGrid, [Tile.Tile], Rand.StdGen)
newGame level gen = (emptyGrid, lst, lastGen)
  where
    emptyGrid = LGridMap.empty $ HexGrid.hexHexGrid (level + 1)
    (fullGrid, middleGen) = foldl' matchSides (emptyGrid, gen) $ Grid.edges emptyGrid
    (randInts, lastGen) = randomInts (indicesNum emptyGrid) middleGen
    lst = map rotateAndTake . sortOn fst . zip randInts $ GridMap.elems fullGrid

matchSides :: (TileGrid, Rand.StdGen) -> (Index, Index) -> (TileGrid, Rand.StdGen)
matchSides (tileGrid, gen) (idxA, idxB) = case (sideA, sideB) of
  (Nothing, _) -> matchSides (putTile newTile idxA tileGrid, gen) (idxA, idxB)
  (_, Nothing) -> matchSides (putTile newTile idxB tileGrid, gen) (idxA, idxB)
  (Just 0, Just 0) -> (adjustSide dirA newVal idxA $ adjustSide dirB newVal idxB tileGrid, newGen)
  (Just n, Just k) -> ((if n /= 0 then adjustSide dirB n idxB else adjustSide dirA k idxA) tileGrid, gen)
  where
    dirA = head $ Grid.directionTo (GridMap.toGrid tileGrid) idxA idxB
    dirB = oppositeDirection dirA
    sideA = Tile.sideValue dirA <$> GridMap.lookup idxA tileGrid
    sideB = Tile.sideValue dirB <$> GridMap.lookup idxB tileGrid
    (newVal, newGen) = Rand.randomR (1,6) gen
    newTile = Tile.empty $ tileSize tileGrid

adjustSide :: HexDirection -> Int -> Index -> TileGrid -> TileGrid
adjustSide dir val idx = GridMap.adjust (Tile.changeSide dir val) idx

randomInts :: Int -> Rand.StdGen -> ([Int], Rand.StdGen)
randomInts 0 gen = ([], gen)
randomInts n gen = (val : follRand, follGen)
  where
    (val, nextGen) = Rand.random gen
    (follRand, follGen) = randomInts (n-1) nextGen

rotateAndTake :: (Int, Tile.Tile) -> Tile.Tile
rotateAndTake (n, tile) = (!! mod n 3) $ iterate Tile.rotate tile

-- rendering functions
render :: Color.Color -> TileGrid -> Pict.Picture
render col tileGrid
  | isEmpty tileGrid = Pict.color col $ Hex.hexagonSolidPointy 230
  | otherwise = Pict.pictures . map (renderIndex col tileGrid) $ indices tileGrid

renderIndex :: Color.Color -> TileGrid -> Index ->  Pict.Picture
renderIndex col tileGrid idx = case GridMap.lookup idx tileGrid of
  Just tile -> Tile.render tile
  _ -> renderEmpty col idx $ tileSize tileGrid

renderEmpty :: Color.Color -> Index -> Float -> Pict.Picture
renderEmpty col idx rad = Pict.color col $ Hex.render hex
  where hex = Hex.Hexagon {Hex.center = indexCenter idx rad, Hex.radius = rad-1}

-- manipulation functions
putTile :: Tile.Tile -> Index -> TileGrid -> TileGrid
putTile tile idx tileGrid = GridMap.insert idx fixedTile tileGrid
  where fixedTile = Tile.moveTo (indexCenter idx (tileSize tileGrid)) tile

grab :: Pict.Point -> TileGrid -> (TileGrid, Maybe Tile.Tile)
grab point tileGrid = case pointToIndex point tileGrid of
  Just idx -> grabIndex idx tileGrid
  _ -> (tileGrid, Nothing)

grabIndex :: Index -> TileGrid -> (TileGrid, Maybe Tile.Tile)
grabIndex idx tileGrid = case GridMap.lookup idx tileGrid of
  Just tile -> (GridMap.delete idx tileGrid, Just tile)
  _ -> (tileGrid, Nothing)

isFull :: TileGrid -> Bool
isFull tileGrid = indicesNum tileGrid == length (GridMap.toList tileGrid)

isCompleted :: TileGrid -> Bool
isCompleted tileGrid
  | isFull tileGrid = all (matchingSides tileGrid) . Grid.edges $ GridMap.toGrid tileGrid
  | otherwise = False

-- utility functions
isEmpty :: TileGrid -> Bool
isEmpty = Grid.null . GridMap.toGrid

indexIsEmpty :: Index -> TileGrid -> Bool
indexIsEmpty idx = Map.notMember idx . GridMap.toMap

indicesNum :: TileGrid -> Int
indicesNum = Grid.tileCount . GridMap.toGrid

indices :: TileGrid -> [Index]
indices = Grid.indices . GridMap.toGrid

indexCenter :: Index -> Float -> Pict.Point -- NOTE: based on odd-q
indexCenter (x, y) radius = (radius * column * 1.5, (offset + row) * (-height))
  where
    column = fromIntegral x
    row = fromIntegral $ (-x) - y + (x - (if odd x then 1 else 0)) `div` 2
    height = Hex.heightFromRadius radius
    offset = if odd x then 0.5 else 0

pointToIndex :: Pict.Point -> TileGrid -> Maybe Index
pointToIndex (x, y) tileGrid
  | Grid.contains tileGrid idx = Just idx
  | otherwise = Nothing
  where
    size = tileSize tileGrid
    idxX = 2/3 * x / size
    idxZ = ((-1)/3 * x + sqrt 3 / 3 * (-y)) / size
    idxY = (-idxX) - idxZ
    idx = (round idxX, round idxY)

matchingSides :: TileGrid -> (Index, Index) -> Bool
matchingSides tileGrid (idxA, idxB) = case (look idxA, look idxB) of
  (Just tileA, Just tileB) -> Tile.sideValue dirA tileA == Tile.sideValue dirB tileB
  _ -> False
  where 
    look = (`GridMap.lookup` tileGrid)
    dirA = head $ Grid.directionTo (GridMap.toGrid tileGrid) idxA idxB
    dirB = oppositeDirection dirA

tileSize :: TileGrid -> Float
tileSize tileGrid = 418 / (2 * rg  + (rg-1))
  where rg = fromIntegral . Grid.size $ GridMap.toGrid tileGrid

oppositeDirection :: HexDirection -> HexDirection
oppositeDirection car = case car of
  North -> South
  Northeast -> Southwest
  Southeast -> Northwest
  South -> North
  Southwest -> Northeast
  Northwest -> Southeast